From 834a9824e5523fcc592613008706613fc9da000b Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 2 Dec 2021 17:07:37 +0100 Subject: [PATCH 01/40] Initial commit --- .github/workflows/php.yml | 80 +++++++ .gitignore | 4 + composer.json | 35 ++++ phpunit.xml.dist | 26 +++ psalm.xml | 16 ++ src/Aggregate/ArrayConstraint.php | 64 ++++++ src/Aggregate/Count.php | 54 +++++ src/Aggregate/ElementType.php | 70 +++++++ src/AnnotationForm.php | 115 ++++++++++ .../ButtonBuilderAnnotationInterface.php | 23 ++ src/Button/Groups.php | 57 +++++ src/Button/Value.php | 47 +++++ src/Child/CallbackModelTransformer.php | 127 +++++++++++ src/Child/Configure.php | 49 +++++ src/Child/DefaultValue.php | 46 ++++ src/Child/Dependencies.php | 54 +++++ src/Child/GetSet.php | 59 ++++++ src/Child/ModelTransformer.php | 57 +++++ src/ChildBuilderAnnotationInterface.php | 24 +++ src/Constraint/CustomConstraint.php | 82 ++++++++ src/Constraint/Satisfy.php | 59 ++++++ src/Element/CallbackTransformer.php | 128 ++++++++++++ src/Element/Choices.php | 113 ++++++++++ src/Element/IgnoreTransformerException.php | 55 +++++ src/Element/Transformer.php | 60 ++++++ src/Element/TransformerError.php | 82 ++++++++ src/Form/CallbackGenerator.php | 55 +++++ src/Form/FormBuilderAnnotationInterface.php | 23 ++ src/Form/Generates.php | 51 +++++ tests/Aggregate/ArrayConstraintTest.php | 30 +++ tests/Aggregate/CountTest.php | 33 +++ tests/Aggregate/ElementTypeTest.php | 86 ++++++++ tests/Button/GroupsTest.php | 25 +++ tests/Button/ValueTest.php | 25 +++ tests/Child/CallbackModelTransformerTest.php | 50 +++++ tests/Child/ConfigureTest.php | 39 ++++ tests/Child/DefaultValueTest.php | 25 +++ tests/Child/GetSetTest.php | 29 +++ tests/Child/ModelTransformerTest.php | 100 +++++++++ tests/Constraint/CustomConstraintTest.php | 34 +++ tests/Element/CallbackTransformerTest.php | 50 +++++ tests/Element/ChoicesTest.php | 48 +++++ .../IgnoreTransformerExceptionTest.php | 36 ++++ tests/Element/TransformerErrorTest.php | 72 +++++++ tests/Form/CallbackGeneratorTest.php | 29 +++ tests/Form/GeneratesTest.php | 82 ++++++++ tests/FunctionalTest.php | 197 ++++++++++++++++++ 47 files changed, 2705 insertions(+) create mode 100644 .github/workflows/php.yml create mode 100644 .gitignore create mode 100644 composer.json create mode 100755 phpunit.xml.dist create mode 100644 psalm.xml create mode 100644 src/Aggregate/ArrayConstraint.php create mode 100644 src/Aggregate/Count.php create mode 100644 src/Aggregate/ElementType.php create mode 100644 src/AnnotationForm.php create mode 100644 src/Button/ButtonBuilderAnnotationInterface.php create mode 100644 src/Button/Groups.php create mode 100644 src/Button/Value.php create mode 100644 src/Child/CallbackModelTransformer.php create mode 100644 src/Child/Configure.php create mode 100644 src/Child/DefaultValue.php create mode 100644 src/Child/Dependencies.php create mode 100644 src/Child/GetSet.php create mode 100644 src/Child/ModelTransformer.php create mode 100644 src/ChildBuilderAnnotationInterface.php create mode 100644 src/Constraint/CustomConstraint.php create mode 100644 src/Constraint/Satisfy.php create mode 100644 src/Element/CallbackTransformer.php create mode 100644 src/Element/Choices.php create mode 100644 src/Element/IgnoreTransformerException.php create mode 100644 src/Element/Transformer.php create mode 100644 src/Element/TransformerError.php create mode 100644 src/Form/CallbackGenerator.php create mode 100644 src/Form/FormBuilderAnnotationInterface.php create mode 100644 src/Form/Generates.php create mode 100644 tests/Aggregate/ArrayConstraintTest.php create mode 100644 tests/Aggregate/CountTest.php create mode 100644 tests/Aggregate/ElementTypeTest.php create mode 100644 tests/Button/GroupsTest.php create mode 100644 tests/Button/ValueTest.php create mode 100644 tests/Child/CallbackModelTransformerTest.php create mode 100644 tests/Child/ConfigureTest.php create mode 100644 tests/Child/DefaultValueTest.php create mode 100644 tests/Child/GetSetTest.php create mode 100644 tests/Child/ModelTransformerTest.php create mode 100644 tests/Constraint/CustomConstraintTest.php create mode 100644 tests/Element/CallbackTransformerTest.php create mode 100644 tests/Element/ChoicesTest.php create mode 100644 tests/Element/IgnoreTransformerExceptionTest.php create mode 100644 tests/Element/TransformerErrorTest.php create mode 100644 tests/Form/CallbackGeneratorTest.php create mode 100644 tests/Form/GeneratesTest.php create mode 100644 tests/FunctionalTest.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..1a8ad78 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ['8.0'] + name: PHP ${{ matrix.php-versions }} + + steps: + - uses: actions/checkout@v2 + + - name: Set Timezone + uses: szenius/set-timezone@v1.0 + with: + timezoneLinux: "Europe/Paris" + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: json + ini-values: date.timezone=Europe/Paris + - name: Check PHP Version + run: php -v + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" + # Docs: https://getcomposer.org/doc/articles/scripts.md + + - name: Run test suite + run: composer run-script tests + + analysis: + name: Analysis + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set Timezone + uses: szenius/set-timezone@v1.0 + with: + timezoneLinux: "Europe/Paris" + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + extensions: json + ini-values: date.timezone=Europe/Paris +# +# - name: Install Infection +# run: composer global require infection/infection + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run type coverage + run: composer run-script psalm +# +# - name: Run Infection +# run: | +# git fetch --depth=1 origin $GITHUB_BASE_REF +# ~/.composer/vendor/bin/infection --logger-github --git-diff-filter=AM diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97a6a13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +vendor/ +composer.lock +.phpunit.result.cache diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..83b9c94 --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "b2pweb/bdf-form-attribute", + "authors": [ + { + "name": "Vincent Quatrevieux", + "email": "vquatrevieux@b2pweb.com" + } + ], + "license": "MIT", + "type": "library", + "autoload": { + "psr-4": { + "Bdf\\Form\\Annotation\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Form\\Annotation\\": "tests" + } + }, + "minimum-stability": "dev", + "require": { + "php": "8.0.*", + "b2pweb/bdf-form": "dev-prepare-attributes" + }, + "require-dev": { + "phpunit/phpunit": "~9.5", + "vimeo/psalm": "~4.13.1" + }, + "scripts": { + "tests": "phpunit", + "tests-with-coverage": "phpunit --coverage-clover coverage.xml", + "psalm": "psalm --shepherd" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100755 index 0000000..7e7e2e5 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + ./src + + + + + + + + ./tests + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..fe8c94d --- /dev/null +++ b/psalm.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/src/Aggregate/ArrayConstraint.php b/src/Aggregate/ArrayConstraint.php new file mode 100644 index 0000000..c4cf5c7 --- /dev/null +++ b/src/Aggregate/ArrayConstraint.php @@ -0,0 +1,64 @@ + + * $builder->array('values')->arrayConstraints(MyConstraint::class, $options); + * + * + * @todo custom array constraint + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[ArrayConstraint(Unique::class, ['message' => 'My error'])] + * private ArrayElement $values; + * } + * + * + * @see Satisfy Attribute for add constraint for items + * @see ArrayElementBuilder::arrayConstraint() The called method + * + * @implements ChildBuilderAnnotationInterface + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class ArrayConstraint implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The constraint class name + * + * @var class-string + */ + public string $constraint, + + /** + * Constraint's constructor options + * + * @var mixed|null + */ + public mixed $options = null + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->arrayConstraint($this->constraint, $this->options); + } +} diff --git a/src/Aggregate/Count.php b/src/Aggregate/Count.php new file mode 100644 index 0000000..fe1a46e --- /dev/null +++ b/src/Aggregate/Count.php @@ -0,0 +1,54 @@ + + * $builder->array('values')->count(['min' => 3]); + * $builder->array('values')->arrayConstraint(new Count(min: 3)); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[Count(min: 3, max: 42)] + * private ArrayElement $values; + * } + * + * + * @see CountConstraint The used constraint + * @see ArrayElementBuilder::arrayConstraint() The called method + * @see ArrayElementBuilder::count() Equivalent method call + * + * @implements ChildBuilderAnnotationInterface + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Count extends CountConstraint implements ChildBuilderAnnotationInterface +{ + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->arrayConstraint($this); + } + + /** + * {@inheritdoc} + */ + public function validatedBy(): string + { + return CountConstraint::class . 'Validator'; + } +} diff --git a/src/Aggregate/ElementType.php b/src/Aggregate/ElementType.php new file mode 100644 index 0000000..a9bbd3d --- /dev/null +++ b/src/Aggregate/ElementType.php @@ -0,0 +1,70 @@ + + * $builder->array('values')->element(IntegerElement::class, [$this, 'myConfigurator']); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[ElementType(IntegerElement::class, 'configureValueItem')] + * private ArrayElement $values; + * + * // The method must be public and take the builder as parameter + * public function configureValueItem(IntegerElementBuilder $builder) + * { + * $builder->min(5); // Configure the element + * } + * } + * + * + * @see ArrayElementBuilder::element() The called method + * + * @implements ChildBuilderAnnotationInterface + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +class ElementType implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The form element class name + * + * @var class-string + */ + public string $elementType, + + /** + * The element configuration method name + * This method must be defined on the form class, and with public visibility + * + * @var string|null + */ + public ?string $configurator = null + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $configurator = $this->configurator ? [$form, $this->configurator] : null; + $builder->element($this->elementType, $configurator); + } +} diff --git a/src/AnnotationForm.php b/src/AnnotationForm.php new file mode 100644 index 0000000..025fbd0 --- /dev/null +++ b/src/AnnotationForm.php @@ -0,0 +1,115 @@ + + */ + private array $elementProperties = []; + + /** + * @var array + */ + private array $buttonProperties = []; + + /** + * {@inheritdoc} + */ + protected function configure(FormBuilderInterface $builder): void + { + // @todo extraire dans une "annotation processor" + + for ($reflection = new ReflectionClass($this); $reflection->getName() !== AnnotationForm::class; $reflection = $reflection->getParentClass()) { + foreach ($reflection->getAttributes() as $attribute) { + if (is_subclass_of($attribute->getName(), FormBuilderAnnotationInterface::class)) { + $attribute->newInstance()->applyOnFormBuilder($this, $builder); + } + } + + foreach ($reflection->getProperties() as $property) { + if (!$property->hasType() || isset($this->elementProperties[$property->getName()]) || isset($this->buttonProperties[$property->getName()])) { + continue; + } + + if (!$property->getType() instanceof ReflectionNamedType) { + continue; + } + + $elementType = $property->getType()->getName(); + + // @todo is_subclass_of($elementType, ButtonInterface::class) ? + if ($elementType === ButtonInterface::class) { + $property->setAccessible(true); + $this->buttonProperties[$property->getName()] = $property; + + $submitBuilder = $builder->submit($property->getName()); + + foreach ($property->getAttributes() as $attribute) { + match (true) { + is_subclass_of($attribute->getName(), ButtonBuilderAnnotationInterface::class) => $attribute->newInstance()->applyOnButtonBuilder($this, $submitBuilder), + }; + } + + continue; + } + + if (!is_subclass_of($elementType, ElementInterface::class)) { + continue; + } + + $property->setAccessible(true); + $this->elementProperties[$property->getName()] = $property; + + $elementBuilder = $builder->add($property->getName(), $elementType); + + foreach ($property->getAttributes() as $attribute) { + match (true) { + is_subclass_of($attribute->getName(), ChildBuilderAnnotationInterface::class) => $attribute->newInstance()->applyOnChildBuilder($this, $elementBuilder), + + is_subclass_of($attribute->getName(), Constraint::class) => $elementBuilder->satisfy($attribute->newInstance()), + is_subclass_of($attribute->getName(), FilterInterface::class) => $elementBuilder->filter($attribute->newInstance()), + is_subclass_of($attribute->getName(), TransformerInterface::class) => $elementBuilder->transformer($attribute->newInstance()), + is_subclass_of($attribute->getName(), HydratorInterface::class) => $elementBuilder->hydrator($attribute->newInstance()), + is_subclass_of($attribute->getName(), ExtractorInterface::class) => $elementBuilder->extractor($attribute->newInstance()), + }; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function postConfigure(FormInterface $form): void + { + foreach ($this->elementProperties as $name => $reflection) { + $reflection->setValue($this, $form[$name]->element()); + } + + foreach ($this->buttonProperties as $name => $reflection) { + $reflection->setValue($this, $this->root()->button($name)); + } + + unset($this->elementProperties); + } +} diff --git a/src/Button/ButtonBuilderAnnotationInterface.php b/src/Button/ButtonBuilderAnnotationInterface.php new file mode 100644 index 0000000..96e97f2 --- /dev/null +++ b/src/Button/ButtonBuilderAnnotationInterface.php @@ -0,0 +1,23 @@ + + * $builder->button('btn')->groups(['Foo', 'Bar']); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[Groups('Foo', 'Bar')] + * private ButtonInterface $btn; + * } + * + * + * @see ButtonBuilderInterface::groups() The called method + * @see ButtonInterface::constraintGroups() Modify this value + * @see RootElementInterface::constraintGroups() + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Groups implements ButtonBuilderAnnotationInterface +{ + /** + * @var list + */ + public array $groups; + + /** + * @param string ...$groups List of validation groups + */ + public function __construct(string ...$groups) + { + $this->groups = $groups; + } + + /** + * {@inheritdoc} + */ + public function applyOnButtonBuilder(AnnotationForm $form, ButtonBuilderInterface $builder): void + { + $builder->groups($this->groups); + } +} diff --git a/src/Button/Value.php b/src/Button/Value.php new file mode 100644 index 0000000..b7f7453 --- /dev/null +++ b/src/Button/Value.php @@ -0,0 +1,47 @@ + + * $builder->button('btn')->value('Foo'); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[Value('Foo')] + * private ButtonInterface $btn; + * } + * + * + * @see ButtonBuilderInterface::value() The called method + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Value implements ButtonBuilderAnnotationInterface +{ + public function __construct( + /** + * The button HTTP value + */ + public string $value, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnButtonBuilder(AnnotationForm $form, ButtonBuilderInterface $builder): void + { + $builder->value($this->value); + } +} diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php new file mode 100644 index 0000000..c510f55 --- /dev/null +++ b/src/Child/CallbackModelTransformer.php @@ -0,0 +1,127 @@ + + * // For unified callback + * $builder->string('foo')->modelTransformer([$this, 'myTransformer']); + * + * // When using two methods (toEntity: 'transformFooToEntity', toInput: 'transformFooToInput') + * $builder->string('foo')->modelTransformer(function ($value, ElementInterface $input, bool $toEntity) { + * return $toEntity ? $this->transformFooToEntity($value, $input) : $this->transformFooToInput($value, $input); + * }); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[CallbackModelTransformer(toEntity: 'fooToModel', toInput: 'fooToInput')] + * private IntegerElement $foo; + * + * // With unified transformer (same as above) + * #[CallbackModelTransformer('barTransformer')] + * private IntegerElement $bar; + * + * public function fooToModel(int $value, IntegerElement $input): string + * { + * return dechex($value); + * } + * + * public function fooToInput(string $value, IntegerElement $input): int + * { + * return hexdec($value); + * } + * + * public function barTransformer($value, IntegerElement $input, bool $toEntity) + * { + * return $toEntity ? dechex($value) : hexdec($value); + * } + * } + * + * + * @see ChildBuilderInterface::modelTransformer() The called method + * @see ModelTransformer For use a transformer class as model transformer + * @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class CallbackModelTransformer implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * Method name use to define the unified transformer method + * If defined, the other parameters will be ignored + */ + public ?string $callback = null, + + /** + * Method name use to define the transformation process from input value to the entity + */ + public ?string $toEntity = null, + + /** + * Method name use to define the transformation process from entity value to input + */ + public ?string $toInput = null, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + if ($this->callback) { + $builder->modelTransformer([$form, $this->callback]); + return; + } + + $builder->modelTransformer(new class($form, $this) implements TransformerInterface { + public function __construct( + private AnnotationForm $form, + private CallbackModelTransformer $annotation, + ) {} + + /** + * {@inheritdoc} + */ + public function transformToHttp($value, ElementInterface $input) + { + if (!$this->annotation->toInput) { + return $value; + } + + return $this->form->{$this->annotation->toInput}($value, $input); + } + + /** + * {@inheritdoc} + */ + public function transformFromHttp($value, ElementInterface $input) + { + if (!$this->annotation->toEntity) { + return $value; + } + + return $this->form->{$this->annotation->toEntity}($value, $input); + } + }); + } +} diff --git a/src/Child/Configure.php b/src/Child/Configure.php new file mode 100644 index 0000000..c691c52 --- /dev/null +++ b/src/Child/Configure.php @@ -0,0 +1,49 @@ + + * class MyForm extends AnnotationForm + * { + * #[Configure('configureFoo')] + * private IntegerElement $foo; + * + * public function configureFoo(ChildBuilderInterface $builder): void + * { + * $builder->min(5); + * } + * } + * + */ +// @todo date time after et before annotations +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +class Configure implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The method name to use as configurator + * The method should follow the prototype `public function (ChildBuilderInterface $builder): void` + */ + public string $callback + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $form->{$this->callback}($builder); + } +} diff --git a/src/Child/DefaultValue.php b/src/Child/DefaultValue.php new file mode 100644 index 0000000..8f9a733 --- /dev/null +++ b/src/Child/DefaultValue.php @@ -0,0 +1,46 @@ + + * $builder->float('foo')->default(12.3); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[DefaultValue(12.3)] + * private FloatElement $foo; + * } + * + * + * @see ChildBuilderInterface::default() The called method + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class DefaultValue implements ChildBuilderAnnotationInterface +{ + public function __construct( + public mixed $default + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->default($this->default); + } +} diff --git a/src/Child/Dependencies.php b/src/Child/Dependencies.php new file mode 100644 index 0000000..0eaa1e0 --- /dev/null +++ b/src/Child/Dependencies.php @@ -0,0 +1,54 @@ + + * $builder->float('foo')->depends('bar', 'baz'); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[Dependencies('bar', 'rab')] + * private FloatElement $foo; + * private IntegerElement $bar; + * private StringElement $rab; + * } + * + * + * @see ChildBuilderInterface::depends() The called method + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Dependencies implements ChildBuilderAnnotationInterface +{ + /** + * @var list + */ + public array $dependencies; + + /** + * @param string ...$dependencies List of inputs names + */ + public function __construct(string ...$dependencies) + { + $this->dependencies = $dependencies; + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->depends(...$this->dependencies); + } +} diff --git a/src/Child/GetSet.php b/src/Child/GetSet.php new file mode 100644 index 0000000..fc8bd93 --- /dev/null +++ b/src/Child/GetSet.php @@ -0,0 +1,59 @@ + + * $builder->float('foo')->getter('bar')->setter('bar'); + * $builder->float('foo')->hydrator(new Setter('bar'))->extract(new Getter('bar')); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[GetSet('bar')] + * private FloatElement $foo; + * } + * + * + * @see ChildBuilder::getter() + * @see ChildBuilder::setter() + * @see ChildBuilderInterface::hydrator() + * @see ChildBuilderInterface::extractor() + * + * @see Getter For define only the extractor + * @see Setter For define only the hydrator + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class GetSet implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The property name to use + * This can be a public property, or public accessor method + * (optionally starting with get for the getter, and starting with set for the setter) + * + * If not provided, the input name will be used as property name + */ + public ?string $propertyName = null, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->hydrator(new Setter($this->propertyName))->extractor(new Getter($this->propertyName)); + } +} diff --git a/src/Child/ModelTransformer.php b/src/Child/ModelTransformer.php new file mode 100644 index 0000000..8a949a8 --- /dev/null +++ b/src/Child/ModelTransformer.php @@ -0,0 +1,57 @@ + + * $builder->string('foo')->modelTransformer(new MyTransformer(...$arguments)); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[ModelTransformer(MyTransformer::class, ['foo', 'bar'])] + * private IntegerElement $foo; + * } + * + * + * @see ChildBuilderInterface::modelTransformer() The called method + * @see CallbackModelTransformer For use custom methods as transformer instead of class + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class ModelTransformer implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The transformer class name + * + * @var class-string + */ + public string $transformerClass, + + /** + * Arguments to provide on the transformer constructor + * + * @var array + */ + public array $constructorArguments = [], + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->modelTransformer(new $this->transformerClass(...$this->constructorArguments)); + } +} diff --git a/src/ChildBuilderAnnotationInterface.php b/src/ChildBuilderAnnotationInterface.php new file mode 100644 index 0000000..55e01e0 --- /dev/null +++ b/src/ChildBuilderAnnotationInterface.php @@ -0,0 +1,24 @@ + $builder + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void; +} diff --git a/src/Constraint/CustomConstraint.php b/src/Constraint/CustomConstraint.php new file mode 100644 index 0000000..d8f67e0 --- /dev/null +++ b/src/Constraint/CustomConstraint.php @@ -0,0 +1,82 @@ + + * $builder->integer('foo')->satisfy([$this, 'validateFoo'], 'Foo is invalid'); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[CustomConstraint('validateFoo', message: 'Foo is invalid')] + * private IntegerElement $foo; + * + * public function validateFoo($value, ElementInterface $input): bool + * { + * return $value % 5 > 2; + * } + * } + * + * + * @see ElementBuilderInterface::satisfy() The called method + * @see Constraint + * @see Closure The used constraint + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class CustomConstraint implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The method name to use as validator + * Must be a public method declared on the form class + * + * Its prototype should be : + * `public function ($value, ElementInterface $input): bool|string|array{code: string message: string}|null` + * + * - Return true, or null (nothing) for a valid input + * - Return false for invalid input, with the default error message (or the declared one) + * - Return string for a custom error message + * - Return array with error message and code + */ + public string $methodName, + + /** + * The error message to use + * This option is used only if the validator return false, in other cases, + * the message returned by the validator will be used + * + * @var string|null + */ + public ?string $message = null, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $constraint = new Closure(['callback' => [$form, $this->methodName]]); + + if ($this->message) { + $constraint->message = $this->message; + } + + $builder->satisfy($constraint); + } +} diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php new file mode 100644 index 0000000..b712fb1 --- /dev/null +++ b/src/Constraint/Satisfy.php @@ -0,0 +1,59 @@ + + * $builder->integer('foo')->satisfy(MyConstraint::class, $options); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[Satisfy(MyConstraint::class, ['foo' => 'bar'])] + * private IntegerElement $foo; + * } + * + * + * @see ElementBuilderInterface::satisfy() The called method + * @see Constraint + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +class Satisfy implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The constraint class name + * + * @var class-string + */ + public string $constraint, + + /** + * Constraint's constructor options + * + * @var mixed|null + */ + public mixed $options = null + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->satisfy($this->constraint, $this->options); + } +} diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php new file mode 100644 index 0000000..e3d2452 --- /dev/null +++ b/src/Element/CallbackTransformer.php @@ -0,0 +1,128 @@ + + * // For unified callback + * $builder->string('foo')->transformer([$this, 'myTransformer']); + * + * // When using two methods (toHttp: 'transformFooToHttp', fromHttp: 'transformFooFromHttp') + * $builder->string('foo')->transformer(function ($value, ElementInterface $input, bool $toPhp) { + * return $toPhp ? $this->transformFooFromHttp($value, $input) : $this->transformFooToHttp($value, $input); + * }); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[CallbackTransformer(fromHttp: 'fooFromHttp', toHttp: 'fooToHttp')] + * private IntegerElement $foo; + * + * // With unified transformer (same as above) + * #[CallbackTransformer('barTransformer')] + * private IntegerElement $bar; + * + * public function fooFromHttp(string $value, IntegerElement $input): int + * { + * return hexdec($value); + * } + * + * public function fooToHttp(int $value, IntegerElement $input): string + * { + * return dechex($value); + * } + * + * public function barTransformer($value, IntegerElement $input, bool $toPhp) + * { + * return $toPhp ? hexdec($value) : dechex($value); + * } + * } + * + * + * @see ElementBuilderInterface::transformer() The called method + * @see Transformer For use a transformer class as transformer + * @see CallbackModelTransformer For use transformer in same way, but for model transformer intead of http one + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class CallbackTransformer implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * Method name use to define the unified transformer method + * If defined, the other parameters will be ignored + */ + public ?string $callback = null, + + /** + * Method name use to define the transformation process from http value to the input + */ + public ?string $fromHttp = null, + + /** + * Method name use to define the transformation process from input value to http format + */ + public ?string $toHttp = null, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + if ($this->callback) { + $builder->transformer([$form, $this->callback]); + return; + } + + $builder->transformer(new class($form, $this) implements TransformerInterface { + public function __construct( + private AnnotationForm $form, + private CallbackTransformer $annotation, + ) {} + + /** + * {@inheritdoc} + */ + public function transformToHttp($value, ElementInterface $input) + { + if (!$this->annotation->toHttp) { + return $value; + } + + return $this->form->{$this->annotation->toHttp}($value, $input); + } + + /** + * {@inheritdoc} + */ + public function transformFromHttp($value, ElementInterface $input) + { + if (!$this->annotation->fromHttp) { + return $value; + } + + return $this->form->{$this->annotation->fromHttp}($value, $input); + } + }); + } +} diff --git a/src/Element/Choices.php b/src/Element/Choices.php new file mode 100644 index 0000000..554ee72 --- /dev/null +++ b/src/Element/Choices.php @@ -0,0 +1,113 @@ + + * $builder->string('foo')->choices(['bar', 'baz']); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[Choices(['bar', 'rab'])] + * private StringElement $foo; + * + * #[Choices(['My label' => 'v1', 'Other label' => 'v2'])] + * private StringElement $bar; + * + * #[Choices('loadBazValues', 'Invalid value')] + * private StringElement $baz; + * + * // For dynamic choices, or with complex logic + * public function loadBazValues(): array + * { + * $values = []; + * + * foreach (BazEntity::all() as $baz) { + * $values[$baz->label()] = $baz->id(); + * } + * + * return $values; + * } + * } + * + * + * @see ChoiceBuilderTrait::choices() The called method + * @see Choiceable Supported element type + * @see ArrayChoice Used when an array is given as parameter + * @see LazzyChoice Used when a method name is given as parameter + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Choices implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * Choice provider + * + * Can be a method name for load choices. The method must be public and declared on the form class, + * with the prototype `public function (): array` + * + * If the value is an array, the key will be used as label (displayed value), and the value as real value + * The label is not required. + */ + public string|array $choices, + + /** + * The error message + * If not provided, a default message will be used + * + * @var string|null + */ + public ?string $message = null, + + /** + * Extra constraint options + * + * @var array + */ + public array $options = [], + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $options = $this->options; + + if ($this->message) { + $options['message'] = $options['multipleMessage'] = $this->message; + } + + // Q&D fix for psalm because it does not recognize trait as type + /** @var StringElementBuilder $builder */ + $builder->choices( + is_string($this->choices) ? fn() => $form->{$this->choices}() : $this->choices, + $options + ); + } +} diff --git a/src/Element/IgnoreTransformerException.php b/src/Element/IgnoreTransformerException.php new file mode 100644 index 0000000..ea8c599 --- /dev/null +++ b/src/Element/IgnoreTransformerException.php @@ -0,0 +1,55 @@ + + * $builder->string('foo')->ignoreTransformerException(); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[MyTransformer, IgnoreTransformerException] + * private StringElement $foo; + * } + * + * + * @see ValidatorBuilderTrait::ignoreTransformerException() The called method + * + * @implements ChildBuilderAnnotationInterface + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class IgnoreTransformerException implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * Ignore or not transformer errors + */ + public bool $ignore = true, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->ignoreTransformerException($this->ignore); + } +} diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php new file mode 100644 index 0000000..7f81781 --- /dev/null +++ b/src/Element/Transformer.php @@ -0,0 +1,60 @@ + + * $builder->string('foo')->transformer(new MyTransformer(...$arguments)); + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[Transformer(MyTransformer::class, ['foo', 'bar'])] + * private IntegerElement $foo; + * } + * + * + * @see ElementBuilderInterface::transformer() The called method + * @see CallbackTransformer For use custom methods as transformer instead of class + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +class Transformer implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The transformer class name + * + * @var class-string + */ + public string $transformerClass, + + /** + * Arguments to provide on the transformer constructor + * + * @var array + */ + public array $constructorArguments = [], + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + $builder->transformer(new $this->transformerClass(...$this->constructorArguments)); + } +} diff --git a/src/Element/TransformerError.php b/src/Element/TransformerError.php new file mode 100644 index 0000000..919f07d --- /dev/null +++ b/src/Element/TransformerError.php @@ -0,0 +1,82 @@ + + * $builder->string('foo') + * ->transformerErrorMessage('Foo is in invalid format') + * ->transformerErrorCode('FOO_FORMAT_ERROR') + * ->transformerExceptionValidation([$this, 'fooTransformerExceptionValidation']) + * ; + * + * + * Usage: + * + * class MyForm extends AnnotationForm + * { + * #[MyTransformer, TransformerError(message: 'Foo is in invalid format', code: 'FOO_FORMAT_ERROR')] + * private StringElement $foo; + * } + * + * + * @see ValidatorBuilderTrait::transformerErrorMessage() The called method when message parameter is provided + * @see ValidatorBuilderTrait::transformerErrorCode() The called method when code parameter is provided + * @see ValidatorBuilderTrait::transformerExceptionValidation() The called method when validationCallback parameter is provided + * + * @implements ChildBuilderAnnotationInterface + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +class TransformerError implements ChildBuilderAnnotationInterface +{ + public function __construct( + /** + * The error message to show when transformer fail + */ + public ?string $message = null, + + /** + * The error code to provide when transformer fail + */ + public ?string $code = null, + + /** + * Method name to use for validate the transformer exception + * + * This method must be public and declared on the form class, and follow the prototype : + * `public function ($value, TransformerExceptionConstraint $constraint, ElementInterface $element): bool` + * + * If the method return false, the exception will be ignored + * Else, the method should fill `TransformerExceptionConstraint` with error message and code to provide the custom error + */ + public ?string $validationCallback = null, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + { + if ($this->message) { + $builder->transformerErrorMessage($this->message); + } + + if ($this->code) { + $builder->transformerErrorCode($this->code); + } + + if ($this->validationCallback) { + $builder->transformerExceptionValidation([$form, $this->validationCallback]); + } + } +} diff --git a/src/Form/CallbackGenerator.php b/src/Form/CallbackGenerator.php new file mode 100644 index 0000000..49cbe9d --- /dev/null +++ b/src/Form/CallbackGenerator.php @@ -0,0 +1,55 @@ + + * $builder->generates([$this, 'generateValue']); + * + * + * Usage: + * + * #[CallbackGenerator('generateValue')] + * class MyForm extends AnnotationForm + * { + * public function generateValue(FormInterface $form) + * { + * return new Foo(); + * } + * } + * + * + * @see FormBuilderInterface::generates() The called method + * @see ValueGenerator + * @see Generates For generate with a simple class name + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class CallbackGenerator implements FormBuilderAnnotationInterface +{ + public function __construct( + /** + * The method name use for generate the form value + * This method should be public, and declared on the form class, following the prototype : + * `public function (FormInterface $form): mixed` + */ + public string $callback, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnFormBuilder(AnnotationForm $form, FormBuilderInterface $builder): void + { + $builder->generates([$form, $this->callback]); + } +} diff --git a/src/Form/FormBuilderAnnotationInterface.php b/src/Form/FormBuilderAnnotationInterface.php new file mode 100644 index 0000000..c80d448 --- /dev/null +++ b/src/Form/FormBuilderAnnotationInterface.php @@ -0,0 +1,23 @@ + + * $builder->generates(MyEntity::class); + * + * + * Usage: + * + * #[Generates(MyEntity::class)] + * class MyForm extends AnnotationForm + * { + * // ... + * } + * + * + * @see FormBuilderInterface::generates() The called method + * @see ValueGenerator + * @see CallbackGenerator For generate using a custom method + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class Generates implements FormBuilderAnnotationInterface +{ + public function __construct( + /** + * The entity class name to generate + * + * @var class-string + */ + public string $className, + ) {} + + /** + * {@inheritdoc} + */ + public function applyOnFormBuilder(AnnotationForm $form, FormBuilderInterface $builder): void + { + $builder->generates($this->className); + } +} diff --git a/tests/Aggregate/ArrayConstraintTest.php b/tests/Aggregate/ArrayConstraintTest.php new file mode 100644 index 0000000..e372c84 --- /dev/null +++ b/tests/Aggregate/ArrayConstraintTest.php @@ -0,0 +1,30 @@ + 'Not unique'])] + public ArrayElement $values; + }; + + $form->submit(['values' => ['aaa', 'aaa']]); + $this->assertFalse($form->valid()); + $this->assertEquals(['values' => 'Not unique'], $form->error()->toArray()); + + $form->submit(['values' => ['aaa', 'bbb']]); + $this->assertTrue($form->valid()); + } +} diff --git a/tests/Aggregate/CountTest.php b/tests/Aggregate/CountTest.php new file mode 100644 index 0000000..2b1d9b9 --- /dev/null +++ b/tests/Aggregate/CountTest.php @@ -0,0 +1,33 @@ +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()); + } +} diff --git a/tests/Aggregate/ElementTypeTest.php b/tests/Aggregate/ElementTypeTest.php new file mode 100644 index 0000000..5fe91d7 --- /dev/null +++ b/tests/Aggregate/ElementTypeTest.php @@ -0,0 +1,86 @@ +submit(['values' => ['123', '456', '789']]); + $this->assertTrue($form->valid()); + + $this->assertSame(['values' => [123, 456, 789]], $form->value()); + } + + /** + * + */ + public function test_with_configurator() + { + $form = new class extends AnnotationForm { + #[ElementType(IntegerElement::class, "configureField"), Setter] + public ArrayElement $values; + + public function configureField(IntegerElementBuilder $builder): void + { + $builder->min(200); + } + }; + + $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()); + } + + /** + * + */ + public function test_with_embedded() + { + $form = new class extends AnnotationForm { + #[ElementType(EmbeddedForm::class), Setter] + public ArrayElement $values; + }; + + $form->submit(['values' => [['a' => 'az', 'b' => 'er'], ['a' => 'ty', 'b' => 'ui']]]); + $this->assertTrue($form->valid()); + + $this->assertEquals(['values' => [new Struct('az', 'er'), new Struct('ty', 'ui')]], $form->value()); + } +} + +#[Generates(Struct::class)] +class EmbeddedForm extends AnnotationForm +{ + #[Setter] + public StringElement $a; + #[Setter] + public StringElement $b; +} + +class Struct +{ + public function __construct( + public ?string $a = null, + public ?string $b = null, + ) {} +} diff --git a/tests/Button/GroupsTest.php b/tests/Button/GroupsTest.php new file mode 100644 index 0000000..b41b18e --- /dev/null +++ b/tests/Button/GroupsTest.php @@ -0,0 +1,25 @@ +submit([]); + $this->assertEquals(['Default'], $form->root()->constraintGroups()); + + $form->submit(['btn' => 'ok']); + $this->assertEquals(['foo', 'bar'], $form->root()->constraintGroups()); + } +} diff --git a/tests/Button/ValueTest.php b/tests/Button/ValueTest.php new file mode 100644 index 0000000..199462d --- /dev/null +++ b/tests/Button/ValueTest.php @@ -0,0 +1,25 @@ +assertFalse($form->submit(['button' => 'ok'])->button->clicked()); + $this->assertTrue($form->submit(['button' => 'foo'])->button->clicked()); + } +} diff --git a/tests/Child/CallbackModelTransformerTest.php b/tests/Child/CallbackModelTransformerTest.php new file mode 100644 index 0000000..610e806 --- /dev/null +++ b/tests/Child/CallbackModelTransformerTest.php @@ -0,0 +1,50 @@ +submit(['a' => 'foo', 'b' => '15']); + $this->assertEquals(new Struct(a: 'Zm9v', b: 'f'), $form->value()); + + $form->import(new Struct(a: 'SGVsbG8gV29ybGQgIQ==', b: 'a')); + $this->assertEquals('Hello World !', $form->a->value()); + $this->assertEquals(10, $form->b->value()); + } +} diff --git a/tests/Child/ConfigureTest.php b/tests/Child/ConfigureTest.php new file mode 100644 index 0000000..bc6bfb2 --- /dev/null +++ b/tests/Child/ConfigureTest.php @@ -0,0 +1,39 @@ +length(['min' => 3]); + } + }; + + $form->submit(['foo' => 'a']); + $this->assertFalse($form->valid()); + $this->assertEquals(['foo' => 'This value is too short. It should have 3 characters or more.'], $form->error()->toArray()); + + $form->submit(['foo' => 'abc']); + $this->assertTrue($form->valid()); + } +} diff --git a/tests/Child/DefaultValueTest.php b/tests/Child/DefaultValueTest.php new file mode 100644 index 0000000..4a2a9fa --- /dev/null +++ b/tests/Child/DefaultValueTest.php @@ -0,0 +1,25 @@ +submit([]); + $this->assertSame(42, $form->v->value()); + } +} diff --git a/tests/Child/GetSetTest.php b/tests/Child/GetSetTest.php new file mode 100644 index 0000000..53db302 --- /dev/null +++ b/tests/Child/GetSetTest.php @@ -0,0 +1,29 @@ +submit(['a' => 'z', 'b' => 'e']); + $this->assertSame(['a' => 'z', 'c' => 'e'], $form->value()); + + $form->import(['a' => 'aaa', 'c' => 'ccc']); + $this->assertSame('aaa', $form->a->value()); + $this->assertSame('ccc', $form->b->value()); + } +} diff --git a/tests/Child/ModelTransformerTest.php b/tests/Child/ModelTransformerTest.php new file mode 100644 index 0000000..d363fbd --- /dev/null +++ b/tests/Child/ModelTransformerTest.php @@ -0,0 +1,100 @@ +submit(['a' => 'foo', 'b' => '15']); + $this->assertEquals(new Struct(a: 'Zm9v', b: 'f'), $form->value()); + + $form->import(new Struct(a: 'SGVsbG8gV29ybGQgIQ==', b: 'a')); + $this->assertEquals('Hello World !', $form->a->value()); + $this->assertEquals(10, $form->b->value()); + + $form->submit(['c' => 'bar']); + $this->assertEquals('foo_bar', $form->value()->c); + + $form->import(new Struct(c: 'foo_abc')); + $this->assertEquals('abc', $form->c->value()); + } +} + +class Struct +{ + public function __construct( + public ?string $a = null, + public ?string $b = null, + public ?string $c = 'foo_', + ) {} +} + +class ATransformer implements TransformerInterface +{ + public function transformToHttp($value, ElementInterface $input) + { + return base64_decode($value); + } + + public function transformFromHttp($value, ElementInterface $input) + { + return base64_encode($value); + } +} + +class BTransformer implements TransformerInterface +{ + public function transformToHttp($value, ElementInterface $input) + { + return hexdec($value); + } + + public function transformFromHttp($value, ElementInterface $input) + { + return dechex($value); + } +} + +class TransformerWithArguments implements TransformerInterface +{ + public function __construct(public string $prefix) {} + + public function transformToHttp($value, ElementInterface $input) + { + return substr($value, strlen($this->prefix)); + } + + public function transformFromHttp($value, ElementInterface $input) + { + return $this->prefix . $value; + } + +} diff --git a/tests/Constraint/CustomConstraintTest.php b/tests/Constraint/CustomConstraintTest.php new file mode 100644 index 0000000..a17dc72 --- /dev/null +++ b/tests/Constraint/CustomConstraintTest.php @@ -0,0 +1,34 @@ +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()); + } +} diff --git a/tests/Element/CallbackTransformerTest.php b/tests/Element/CallbackTransformerTest.php new file mode 100644 index 0000000..69b4d43 --- /dev/null +++ b/tests/Element/CallbackTransformerTest.php @@ -0,0 +1,50 @@ +submit(['foo' => 'a', 'bar' => 'b']); + + $this->assertEquals('["a",true]', $form->foo->value()); + $this->assertEquals('["in","b"]', $form->bar->value()); + + $view = $form->view(); + + $this->assertEquals('["[\"a\",true]",false]', $view['foo']->value()); + $this->assertEquals('["out","[\"in\",\"b\"]"]', $view['bar']->value()); + } +} diff --git a/tests/Element/ChoicesTest.php b/tests/Element/ChoicesTest.php new file mode 100644 index 0000000..15052ac --- /dev/null +++ b/tests/Element/ChoicesTest.php @@ -0,0 +1,48 @@ + 2])] + public ArrayElement $bar; + + #[Choices('generateChoices')] + public StringElement $baz; + + public function generateChoices() + { + return ['aaa', 'bbb', 'ccc']; + } + }; + + $form->submit(['foo' => 'a', 'bar' => ['b'], 'baz' => 'c']); + + $this->assertEquals(new ArrayChoice(['foo', 'bar']), $form->foo->choices()); + $this->assertEquals(new ArrayChoice(['foo', 'bar', 'baz']), $form->bar->choices()); + $this->assertEquals(['aaa', 'bbb', 'ccc'], $form->baz->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()); + } +} diff --git a/tests/Element/IgnoreTransformerExceptionTest.php b/tests/Element/IgnoreTransformerExceptionTest.php new file mode 100644 index 0000000..e9d86eb --- /dev/null +++ b/tests/Element/IgnoreTransformerExceptionTest.php @@ -0,0 +1,36 @@ +submit(['foo' => 'a', 'bar' => 'b']); + + $this->assertFalse($form->valid()); + $this->assertEquals(['bar' => 'My error'], $form->error()->toArray()); + } +} diff --git a/tests/Element/TransformerErrorTest.php b/tests/Element/TransformerErrorTest.php new file mode 100644 index 0000000..85d3ba0 --- /dev/null +++ b/tests/Element/TransformerErrorTest.php @@ -0,0 +1,72 @@ +submit(['foo' => 'a', 'bar' => 'b']); + + $this->assertEquals(['foo' => 'my message', 'bar' => 'bar'], $form->error()->toArray()); + $this->assertEquals('BAR_ERROR', $form->error()->children()['bar']->code()); + } + + /** + * + */ + public function test_with_callback() + { + $form = new class extends AnnotationForm { + #[CallbackTransformer('transformer'), TransformerError(validationCallback: 'handleError')] + public StringElement $foo; + + public function transformer() + { + throw new \Exception('My error'); + } + + public function handleError($value, TransformerExceptionConstraint $constraint) + { + if ($value === 'a') { + return false; + } + + $constraint->message = str_repeat($value, 5); + $constraint->code = 'FOO'; + + return true; + } + }; + + $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()); + } +} diff --git a/tests/Form/CallbackGeneratorTest.php b/tests/Form/CallbackGeneratorTest.php new file mode 100644 index 0000000..cc90754 --- /dev/null +++ b/tests/Form/CallbackGeneratorTest.php @@ -0,0 +1,29 @@ + null, 'bar' => 'a']; + } + }; + + $form->submit(['foo' => 'b']); + $this->assertEquals((object) ['foo' => 'b', 'bar' => 'a'], $form->value()); + } +} diff --git a/tests/Form/GeneratesTest.php b/tests/Form/GeneratesTest.php new file mode 100644 index 0000000..5acee34 --- /dev/null +++ b/tests/Form/GeneratesTest.php @@ -0,0 +1,82 @@ +submit(['firstName' => 'John', 'lastName' => 'Doe', 'age' => '35']); + + $expected = new Person(); + $expected->firstName = 'John'; + $expected->lastName = 'Doe'; + $expected->age = 35; + $this->assertEquals($expected, $form->value()); + } +} + +class Person +{ + public string $firstName; + public string $lastName; + public int $age; +} + +class MyForm extends CustomForm +{ + /** + * {@inheritdoc} + */ + protected function configure(FormBuilderInterface $builder): void + { + $builder->add('custom', MyCustomElement::class)->satisfy(new NotEqualTo('15')); + } +} + +// @todo is repetable +class OrderForm extends AnnotationForm +{ + #[Positive(message: 'Valeur incorrecte'), Getter, Setter] + public FloatElement $weight; + + #[Positive(message: 'Valeur incorrecte'), Getter, Setter] + public FloatElement $length; + + #[Positive(message: 'Valeur incorrecte'), Getter, Setter] + public FloatElement $width; + + #[Positive(message: 'Valeur incorrecte'), Getter, Setter] + public FloatElement $height; + + #[Positive(message: 'Valeur incorrecte'), Getter, Setter] + public FloatElement $volume; + + #[Positive(message: 'Valeur incorrecte'), Getter, Setter] + public IntegerElement $palletsCount; + + #[Positive(message: 'Valeur incorrecte'), Getter, Setter] + public IntegerElement $parcelsCount; +} diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php new file mode 100644 index 0000000..4ae4895 --- /dev/null +++ b/tests/FunctionalTest.php @@ -0,0 +1,197 @@ +assertInstanceOf(ChildInterface::class, $form['firstName']); + $this->assertInstanceOf(ChildInterface::class, $form['lastName']); + $this->assertInstanceOf(ChildInterface::class, $form['age']); + + $this->assertInstanceOf(StringElement::class, $form['firstName']->element()); + $this->assertInstanceOf(StringElement::class, $form['lastName']->element()); + $this->assertInstanceOf(IntegerElement::class, $form['age']->element()); + + $this->assertSame($form['firstName']->element(), $form->firstName); + $this->assertSame($form['lastName']->element(), $form->lastName); + $this->assertSame($form['age']->element(), $form->age); + + $form->submit(['firstName' => 'John', 'lastName' => 'Doe', 'age' => '35']); + $this->assertTrue($form->valid()); + + $this->assertSame(['firstName' => 'John', 'lastName' => 'Doe', 'age' => 35], $form->value()); + + $form->submit(['firstName' => 'Foo', 'lastName' => 'B', 'age' => '-5']); + + $this->assertFalse($form->valid()); + $this->assertEquals([ + 'lastName' => 'This value is too short. It should have 3 characters or more.', + 'age' => 'This value should be greater than 0.', + ], $form->error()->toArray()); + } + + /** + * + */ + public function test_setter_with_name() + { + $form = new class extends AnnotationForm { + #[Setter('bar')] + public StringElement $foo; + }; + + $form->submit(['foo' => 'azerty']); + $this->assertSame(['bar' => 'azerty'], $form->value()); + } + + /** + * + */ + public function test_getter_with_name() + { + $form = new class extends AnnotationForm { + #[Getter('bar')] + public StringElement $foo; + }; + + $form->import(['bar' => 'aqw']); + $this->assertSame('aqw', $form->foo->value()); + } + + /** + * + */ + public function test_inheritance() + { + $form = new ChildForm(); + + $this->assertInstanceOf(StringElement::class, $form['foo']->element()); + $this->assertInstanceOf(IntegerElement::class, $form['bar']->element()); + + $form->submit([]); + + $this->assertFalse($form->valid()); + $this->assertEquals(['foo' => 'This value should not be blank.', 'bar' => 'This value should not be blank.'], $form->error()->toArray()); + + $form->submit(['foo' => 'azerty', 'bar' => '25']); + $this->assertTrue($form->valid()); + + $this->assertSame(['bar' => 25, 'foo' => 'azerty'], $form->value()); + } + + /** + * + */ + public function test_buttons() + { + $form = new class extends AnnotationForm { + public ButtonInterface $foo; + public ButtonInterface $bar; + }; + + $form->submit([]); + + $this->assertInstanceOf(SubmitButton::class, $form->foo); + $this->assertInstanceOf(SubmitButton::class, $form->bar); + + $this->assertFalse($form->foo->clicked()); + $this->assertFalse($form->bar->clicked()); + + $form->submit(['foo' => 'ok']); + + $this->assertTrue($form->foo->clicked()); + $this->assertFalse($form->bar->clicked()); + + $form->submit(['bar' => 'ok']); + + $this->assertFalse($form->foo->clicked()); + $this->assertTrue($form->bar->clicked()); + } + + /** + * + */ + public function test_filter() + { + $form = new class extends AnnotationForm { + #[FilterVar(FILTER_SANITIZE_FULL_SPECIAL_CHARS), Setter] + public StringElement $foo; + }; + + $form->submit(['foo' => '&world']); + $this->assertSame(['foo' => '<hello>&world'], $form->value()); + } + + /** + * + */ + public function test_transformer() + { + $form = new class extends AnnotationForm { + #[MyTransformer, Setter] + public StringElement $foo; + }; + + $form->submit(['foo' => 'aaa']); + $this->assertSame(['foo' => 'YWFh'], $form->value()); + } +} + +class BaseForm extends AnnotationForm +{ + #[NotBlank, Getter, Setter] + private StringElement $foo; +} + +class ChildForm extends BaseForm +{ + #[NotBlank, Getter, Setter, GreaterThan(5)] + private IntegerElement $bar; +} + +#[Attribute(Attribute::TARGET_PROPERTY)] +class MyTransformer implements TransformerInterface +{ + public function transformToHttp($value, ElementInterface $input) + { + return base64_decode($value); + } + + public function transformFromHttp($value, ElementInterface $input) + { + return base64_encode($value); + } +} From c932b25c52fe9c14abbaea78a8a3809f4772bd11 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 3 Dec 2021 09:17:23 +0100 Subject: [PATCH 02/40] Add description on composer.json --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index 83b9c94..ebfe7b9 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,7 @@ { "name": "b2pweb/bdf-form-attribute", + "description": "Declaring forms using PHP 8 attributes and typed properties, over bdf-form", + "keywords": ["attributes", "PHP 8", "form", "validator"], "authors": [ { "name": "Vincent Quatrevieux", From 2675e9851f7a2571269971b705c0a75bd75f9fae Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 3 Dec 2021 09:18:39 +0100 Subject: [PATCH 03/40] Use PHP 8.0 for analysis --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 1a8ad78..c8b0363 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -58,7 +58,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.0 extensions: json ini-values: date.timezone=Europe/Paris # From 1c56b2ca57625ecc0d10935b52fd6eab9142a439 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 3 Dec 2021 09:37:03 +0100 Subject: [PATCH 04/40] Rename Annotation to Attribute --- LICENSE | 19 ++++++++++++++ composer.json | 4 +-- src/Aggregate/ArrayConstraint.php | 16 ++++++------ src/Aggregate/Count.php | 14 +++++----- src/Aggregate/ElementType.php | 14 +++++----- src/{AnnotationForm.php => AttributeForm.php} | 18 ++++++------- ...hp => ButtonBuilderAttributeInterface.php} | 10 +++---- src/Button/Groups.php | 10 +++---- src/Button/Value.php | 10 +++---- src/Child/CallbackModelTransformer.php | 26 +++++++++---------- src/Child/Configure.php | 14 +++++----- src/Child/DefaultValue.php | 12 ++++----- src/Child/Dependencies.php | 12 ++++----- src/Child/GetSet.php | 12 ++++----- src/Child/ModelTransformer.php | 12 ++++----- ...php => ChildBuilderAttributeInterface.php} | 8 +++--- src/Constraint/CustomConstraint.php | 12 ++++----- src/Constraint/Satisfy.php | 12 ++++----- src/Element/CallbackTransformer.php | 26 +++++++++---------- src/Element/Choices.php | 12 ++++----- src/Element/IgnoreTransformerException.php | 14 +++++----- src/Element/Transformer.php | 12 ++++----- src/Element/TransformerError.php | 14 +++++----- src/Form/CallbackGenerator.php | 10 +++---- src/Form/FormBuilderAnnotationInterface.php | 23 ---------------- src/Form/FormBuilderAttributeInterface.php | 23 ++++++++++++++++ src/Form/Generates.php | 10 +++---- tests/Aggregate/ArrayConstraintTest.php | 8 +++--- tests/Aggregate/CountTest.php | 8 +++--- tests/Aggregate/ElementTypeTest.php | 16 ++++++------ tests/Button/GroupsTest.php | 8 +++--- tests/Button/ValueTest.php | 8 +++--- tests/Child/CallbackModelTransformerTest.php | 10 +++---- tests/Child/ConfigureTest.php | 8 +++--- tests/Child/DefaultValueTest.php | 8 +++--- tests/Child/GetSetTest.php | 8 +++--- tests/Child/ModelTransformerTest.php | 13 +++++----- tests/Constraint/CustomConstraintTest.php | 8 +++--- tests/Element/CallbackTransformerTest.php | 8 +++--- tests/Element/ChoicesTest.php | 8 +++--- .../IgnoreTransformerExceptionTest.php | 10 +++---- tests/Element/TransformerErrorTest.php | 12 ++++----- tests/Form/CallbackGeneratorTest.php | 8 +++--- tests/Form/GeneratesTest.php | 10 +++---- tests/FunctionalTest.php | 18 ++++++------- 45 files changed, 287 insertions(+), 269 deletions(-) create mode 100644 LICENSE rename src/{AnnotationForm.php => AttributeForm.php} (86%) rename src/Button/{ButtonBuilderAnnotationInterface.php => ButtonBuilderAttributeInterface.php} (56%) rename src/{ChildBuilderAnnotationInterface.php => ChildBuilderAttributeInterface.php} (63%) delete mode 100644 src/Form/FormBuilderAnnotationInterface.php create mode 100644 src/Form/FormBuilderAttributeInterface.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..abad63b --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2021 B2PWeb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/composer.json b/composer.json index ebfe7b9..f46ddf2 100644 --- a/composer.json +++ b/composer.json @@ -12,12 +12,12 @@ "type": "library", "autoload": { "psr-4": { - "Bdf\\Form\\Annotation\\": "src" + "Bdf\\Form\\Attribute\\": "src" } }, "autoload-dev": { "psr-4": { - "Tests\\Form\\Annotation\\": "tests" + "Tests\\Form\\Attribute\\": "tests" } }, "minimum-stability": "dev", diff --git a/src/Aggregate/ArrayConstraint.php b/src/Aggregate/ArrayConstraint.php index c4cf5c7..608aa1a 100644 --- a/src/Aggregate/ArrayConstraint.php +++ b/src/Aggregate/ArrayConstraint.php @@ -1,12 +1,12 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[ArrayConstraint(Unique::class, ['message' => 'My error'])] * private ArrayElement $values; @@ -33,10 +33,10 @@ * @see Satisfy Attribute for add constraint for items * @see ArrayElementBuilder::arrayConstraint() The called method * - * @implements ChildBuilderAnnotationInterface + * @implements ChildBuilderAttributeInterface */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class ArrayConstraint implements ChildBuilderAnnotationInterface +final class ArrayConstraint implements ChildBuilderAttributeInterface { public function __construct( /** @@ -57,7 +57,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->arrayConstraint($this->constraint, $this->options); } diff --git a/src/Aggregate/Count.php b/src/Aggregate/Count.php index fe1a46e..074df30 100644 --- a/src/Aggregate/Count.php +++ b/src/Aggregate/Count.php @@ -1,11 +1,11 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Count(min: 3, max: 42)] * private ArrayElement $values; @@ -31,15 +31,15 @@ * @see ArrayElementBuilder::arrayConstraint() The called method * @see ArrayElementBuilder::count() Equivalent method call * - * @implements ChildBuilderAnnotationInterface + * @implements ChildBuilderAttributeInterface */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Count extends CountConstraint implements ChildBuilderAnnotationInterface +final class Count extends CountConstraint implements ChildBuilderAttributeInterface { /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->arrayConstraint($this); } diff --git a/src/Aggregate/ElementType.php b/src/Aggregate/ElementType.php index a9bbd3d..b015ab9 100644 --- a/src/Aggregate/ElementType.php +++ b/src/Aggregate/ElementType.php @@ -1,11 +1,11 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[ElementType(IntegerElement::class, 'configureValueItem')] * private ArrayElement $values; @@ -37,10 +37,10 @@ * * @see ArrayElementBuilder::element() The called method * - * @implements ChildBuilderAnnotationInterface + * @implements ChildBuilderAttributeInterface */ #[Attribute(Attribute::TARGET_PROPERTY)] -class ElementType implements ChildBuilderAnnotationInterface +class ElementType implements ChildBuilderAttributeInterface { public function __construct( /** @@ -62,7 +62,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $configurator = $this->configurator ? [$form, $this->configurator] : null; $builder->element($this->elementType, $configurator); diff --git a/src/AnnotationForm.php b/src/AttributeForm.php similarity index 86% rename from src/AnnotationForm.php rename to src/AttributeForm.php index 025fbd0..022ed7b 100644 --- a/src/AnnotationForm.php +++ b/src/AttributeForm.php @@ -1,11 +1,11 @@ @@ -37,11 +37,11 @@ abstract class AnnotationForm extends CustomForm */ protected function configure(FormBuilderInterface $builder): void { - // @todo extraire dans une "annotation processor" + // @todo extraire dans une "attribute processor" - for ($reflection = new ReflectionClass($this); $reflection->getName() !== AnnotationForm::class; $reflection = $reflection->getParentClass()) { + for ($reflection = new ReflectionClass($this); $reflection->getName() !== AttributeForm::class; $reflection = $reflection->getParentClass()) { foreach ($reflection->getAttributes() as $attribute) { - if (is_subclass_of($attribute->getName(), FormBuilderAnnotationInterface::class)) { + if (is_subclass_of($attribute->getName(), FormBuilderAttributeInterface::class)) { $attribute->newInstance()->applyOnFormBuilder($this, $builder); } } @@ -66,7 +66,7 @@ protected function configure(FormBuilderInterface $builder): void foreach ($property->getAttributes() as $attribute) { match (true) { - is_subclass_of($attribute->getName(), ButtonBuilderAnnotationInterface::class) => $attribute->newInstance()->applyOnButtonBuilder($this, $submitBuilder), + is_subclass_of($attribute->getName(), ButtonBuilderAttributeInterface::class) => $attribute->newInstance()->applyOnButtonBuilder($this, $submitBuilder), }; } @@ -84,7 +84,7 @@ protected function configure(FormBuilderInterface $builder): void foreach ($property->getAttributes() as $attribute) { match (true) { - is_subclass_of($attribute->getName(), ChildBuilderAnnotationInterface::class) => $attribute->newInstance()->applyOnChildBuilder($this, $elementBuilder), + is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class) => $attribute->newInstance()->applyOnChildBuilder($this, $elementBuilder), is_subclass_of($attribute->getName(), Constraint::class) => $elementBuilder->satisfy($attribute->newInstance()), is_subclass_of($attribute->getName(), FilterInterface::class) => $elementBuilder->filter($attribute->newInstance()), diff --git a/src/Button/ButtonBuilderAnnotationInterface.php b/src/Button/ButtonBuilderAttributeInterface.php similarity index 56% rename from src/Button/ButtonBuilderAnnotationInterface.php rename to src/Button/ButtonBuilderAttributeInterface.php index 96e97f2..ea183cd 100644 --- a/src/Button/ButtonBuilderAnnotationInterface.php +++ b/src/Button/ButtonBuilderAttributeInterface.php @@ -1,8 +1,8 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Groups('Foo', 'Bar')] * private ButtonInterface $btn; @@ -32,7 +32,7 @@ * @see RootElementInterface::constraintGroups() */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Groups implements ButtonBuilderAnnotationInterface +final class Groups implements ButtonBuilderAttributeInterface { /** * @var list @@ -50,7 +50,7 @@ public function __construct(string ...$groups) /** * {@inheritdoc} */ - public function applyOnButtonBuilder(AnnotationForm $form, ButtonBuilderInterface $builder): void + public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface $builder): void { $builder->groups($this->groups); } diff --git a/src/Button/Value.php b/src/Button/Value.php index b7f7453..df9ca08 100644 --- a/src/Button/Value.php +++ b/src/Button/Value.php @@ -1,9 +1,9 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Value('Foo')] * private ButtonInterface $btn; @@ -28,7 +28,7 @@ * @see ButtonBuilderInterface::value() The called method */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Value implements ButtonBuilderAnnotationInterface +final class Value implements ButtonBuilderAttributeInterface { public function __construct( /** @@ -40,7 +40,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnButtonBuilder(AnnotationForm $form, ButtonBuilderInterface $builder): void + public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface $builder): void { $builder->value($this->value); } diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index c510f55..235d978 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -1,11 +1,11 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[CallbackModelTransformer(toEntity: 'fooToModel', toInput: 'fooToInput')] * private IntegerElement $foo; @@ -63,7 +63,7 @@ * @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CallbackModelTransformer implements ChildBuilderAnnotationInterface +final class CallbackModelTransformer implements ChildBuilderAttributeInterface { public function __construct( /** @@ -86,7 +86,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { if ($this->callback) { $builder->modelTransformer([$form, $this->callback]); @@ -95,8 +95,8 @@ public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder->modelTransformer(new class($form, $this) implements TransformerInterface { public function __construct( - private AnnotationForm $form, - private CallbackModelTransformer $annotation, + private AttributeForm $form, + private CallbackModelTransformer $attribute, ) {} /** @@ -104,11 +104,11 @@ public function __construct( */ public function transformToHttp($value, ElementInterface $input) { - if (!$this->annotation->toInput) { + if (!$this->attribute->toInput) { return $value; } - return $this->form->{$this->annotation->toInput}($value, $input); + return $this->form->{$this->attribute->toInput}($value, $input); } /** @@ -116,11 +116,11 @@ public function transformToHttp($value, ElementInterface $input) */ public function transformFromHttp($value, ElementInterface $input) { - if (!$this->annotation->toEntity) { + if (!$this->attribute->toEntity) { return $value; } - return $this->form->{$this->annotation->toEntity}($value, $input); + return $this->form->{$this->attribute->toEntity}($value, $input); } }); } diff --git a/src/Child/Configure.php b/src/Child/Configure.php index c691c52..8e85a0a 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Configure('configureFoo')] * private IntegerElement $foo; @@ -27,9 +27,9 @@ * } * */ -// @todo date time after et before annotations +// @todo date time after et before attributes #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -class Configure implements ChildBuilderAnnotationInterface +class Configure implements ChildBuilderAttributeInterface { public function __construct( /** @@ -42,7 +42,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $form->{$this->callback}($builder); } diff --git a/src/Child/DefaultValue.php b/src/Child/DefaultValue.php index 8f9a733..1a51f84 100644 --- a/src/Child/DefaultValue.php +++ b/src/Child/DefaultValue.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[DefaultValue(12.3)] * private FloatElement $foo; @@ -30,7 +30,7 @@ * @see ChildBuilderInterface::default() The called method */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class DefaultValue implements ChildBuilderAnnotationInterface +final class DefaultValue implements ChildBuilderAttributeInterface { public function __construct( public mixed $default @@ -39,7 +39,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->default($this->default); } diff --git a/src/Child/Dependencies.php b/src/Child/Dependencies.php index 0eaa1e0..b41e7b6 100644 --- a/src/Child/Dependencies.php +++ b/src/Child/Dependencies.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Dependencies('bar', 'rab')] * private FloatElement $foo; @@ -29,7 +29,7 @@ * @see ChildBuilderInterface::depends() The called method */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Dependencies implements ChildBuilderAnnotationInterface +final class Dependencies implements ChildBuilderAttributeInterface { /** * @var list @@ -47,7 +47,7 @@ public function __construct(string ...$dependencies) /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->depends(...$this->dependencies); } diff --git a/src/Child/GetSet.php b/src/Child/GetSet.php index fc8bd93..3d8ab0e 100644 --- a/src/Child/GetSet.php +++ b/src/Child/GetSet.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[GetSet('bar')] * private FloatElement $foo; @@ -36,7 +36,7 @@ * @see Setter For define only the hydrator */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class GetSet implements ChildBuilderAnnotationInterface +final class GetSet implements ChildBuilderAttributeInterface { public function __construct( /** @@ -52,7 +52,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->hydrator(new Setter($this->propertyName))->extractor(new Getter($this->propertyName)); } diff --git a/src/Child/ModelTransformer.php b/src/Child/ModelTransformer.php index 8a949a8..744b78d 100644 --- a/src/Child/ModelTransformer.php +++ b/src/Child/ModelTransformer.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[ModelTransformer(MyTransformer::class, ['foo', 'bar'])] * private IntegerElement $foo; @@ -29,7 +29,7 @@ * @see CallbackModelTransformer For use custom methods as transformer instead of class */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class ModelTransformer implements ChildBuilderAnnotationInterface +final class ModelTransformer implements ChildBuilderAttributeInterface { public function __construct( /** @@ -50,7 +50,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->modelTransformer(new $this->transformerClass(...$this->constructorArguments)); } diff --git a/src/ChildBuilderAnnotationInterface.php b/src/ChildBuilderAttributeInterface.php similarity index 63% rename from src/ChildBuilderAnnotationInterface.php rename to src/ChildBuilderAttributeInterface.php index 55e01e0..3f58efd 100644 --- a/src/ChildBuilderAnnotationInterface.php +++ b/src/ChildBuilderAttributeInterface.php @@ -1,6 +1,6 @@ $builder */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void; + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void; } diff --git a/src/Constraint/CustomConstraint.php b/src/Constraint/CustomConstraint.php index d8f67e0..00dfff5 100644 --- a/src/Constraint/CustomConstraint.php +++ b/src/Constraint/CustomConstraint.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[CustomConstraint('validateFoo', message: 'Foo is invalid')] * private IntegerElement $foo; @@ -39,7 +39,7 @@ * @see Closure The used constraint */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CustomConstraint implements ChildBuilderAnnotationInterface +final class CustomConstraint implements ChildBuilderAttributeInterface { public function __construct( /** @@ -69,7 +69,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $constraint = new Closure(['callback' => [$form, $this->methodName]]); diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index b712fb1..9248356 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Satisfy(MyConstraint::class, ['foo' => 'bar'])] * private IntegerElement $foo; @@ -31,7 +31,7 @@ * @see Constraint */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -class Satisfy implements ChildBuilderAnnotationInterface +class Satisfy implements ChildBuilderAttributeInterface { public function __construct( /** @@ -52,7 +52,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->satisfy($this->constraint, $this->options); } diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index e3d2452..07f54d2 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -1,11 +1,11 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[CallbackTransformer(fromHttp: 'fooFromHttp', toHttp: 'fooToHttp')] * private IntegerElement $foo; @@ -64,7 +64,7 @@ * @see CallbackModelTransformer For use transformer in same way, but for model transformer intead of http one */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CallbackTransformer implements ChildBuilderAnnotationInterface +final class CallbackTransformer implements ChildBuilderAttributeInterface { public function __construct( /** @@ -87,7 +87,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { if ($this->callback) { $builder->transformer([$form, $this->callback]); @@ -96,8 +96,8 @@ public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder->transformer(new class($form, $this) implements TransformerInterface { public function __construct( - private AnnotationForm $form, - private CallbackTransformer $annotation, + private AttributeForm $form, + private CallbackTransformer $attribute, ) {} /** @@ -105,11 +105,11 @@ public function __construct( */ public function transformToHttp($value, ElementInterface $input) { - if (!$this->annotation->toHttp) { + if (!$this->attribute->toHttp) { return $value; } - return $this->form->{$this->annotation->toHttp}($value, $input); + return $this->form->{$this->attribute->toHttp}($value, $input); } /** @@ -117,11 +117,11 @@ public function transformToHttp($value, ElementInterface $input) */ public function transformFromHttp($value, ElementInterface $input) { - if (!$this->annotation->fromHttp) { + if (!$this->attribute->fromHttp) { return $value; } - return $this->form->{$this->annotation->fromHttp}($value, $input); + return $this->form->{$this->attribute->fromHttp}($value, $input); } }); } diff --git a/src/Element/Choices.php b/src/Element/Choices.php index 554ee72..ee793c4 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -1,11 +1,11 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Choices(['bar', 'rab'])] * private StringElement $foo; @@ -62,7 +62,7 @@ * @see LazzyChoice Used when a method name is given as parameter */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Choices implements ChildBuilderAnnotationInterface +final class Choices implements ChildBuilderAttributeInterface { public function __construct( /** @@ -95,7 +95,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $options = $this->options; diff --git a/src/Element/IgnoreTransformerException.php b/src/Element/IgnoreTransformerException.php index ea8c599..d7b5e91 100644 --- a/src/Element/IgnoreTransformerException.php +++ b/src/Element/IgnoreTransformerException.php @@ -1,11 +1,11 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[MyTransformer, IgnoreTransformerException] * private StringElement $foo; @@ -33,10 +33,10 @@ * * @see ValidatorBuilderTrait::ignoreTransformerException() The called method * - * @implements ChildBuilderAnnotationInterface + * @implements ChildBuilderAttributeInterface */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class IgnoreTransformerException implements ChildBuilderAnnotationInterface +final class IgnoreTransformerException implements ChildBuilderAttributeInterface { public function __construct( /** @@ -48,7 +48,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->ignoreTransformerException($this->ignore); } diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index 7f81781..3965ecd 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -1,10 +1,10 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[Transformer(MyTransformer::class, ['foo', 'bar'])] * private IntegerElement $foo; @@ -32,7 +32,7 @@ * @see CallbackTransformer For use custom methods as transformer instead of class */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -class Transformer implements ChildBuilderAnnotationInterface +class Transformer implements ChildBuilderAttributeInterface { public function __construct( /** @@ -53,7 +53,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->transformer(new $this->transformerClass(...$this->constructorArguments)); } diff --git a/src/Element/TransformerError.php b/src/Element/TransformerError.php index 919f07d..089086c 100644 --- a/src/Element/TransformerError.php +++ b/src/Element/TransformerError.php @@ -1,11 +1,11 @@ - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * #[MyTransformer, TransformerError(message: 'Foo is in invalid format', code: 'FOO_FORMAT_ERROR')] * private StringElement $foo; @@ -34,10 +34,10 @@ * @see ValidatorBuilderTrait::transformerErrorCode() The called method when code parameter is provided * @see ValidatorBuilderTrait::transformerExceptionValidation() The called method when validationCallback parameter is provided * - * @implements ChildBuilderAnnotationInterface + * @implements ChildBuilderAttributeInterface */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -class TransformerError implements ChildBuilderAnnotationInterface +class TransformerError implements ChildBuilderAttributeInterface { public function __construct( /** @@ -65,7 +65,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnChildBuilder(AnnotationForm $form, ChildBuilderInterface $builder): void + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { if ($this->message) { $builder->transformerErrorMessage($this->message); diff --git a/src/Form/CallbackGenerator.php b/src/Form/CallbackGenerator.php index 49cbe9d..d88b2f2 100644 --- a/src/Form/CallbackGenerator.php +++ b/src/Form/CallbackGenerator.php @@ -1,11 +1,11 @@ * #[CallbackGenerator('generateValue')] - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * public function generateValue(FormInterface $form) * { @@ -34,7 +34,7 @@ * @see Generates For generate with a simple class name */ #[Attribute(Attribute::TARGET_CLASS)] -final class CallbackGenerator implements FormBuilderAnnotationInterface +final class CallbackGenerator implements FormBuilderAttributeInterface { public function __construct( /** @@ -48,7 +48,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnFormBuilder(AnnotationForm $form, FormBuilderInterface $builder): void + public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void { $builder->generates([$form, $this->callback]); } diff --git a/src/Form/FormBuilderAnnotationInterface.php b/src/Form/FormBuilderAnnotationInterface.php deleted file mode 100644 index c80d448..0000000 --- a/src/Form/FormBuilderAnnotationInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - * #[Generates(MyEntity::class)] - * class MyForm extends AnnotationForm + * class MyForm extends AttributeForm * { * // ... * } @@ -30,7 +30,7 @@ * @see CallbackGenerator For generate using a custom method */ #[Attribute(Attribute::TARGET_CLASS)] -final class Generates implements FormBuilderAnnotationInterface +final class Generates implements FormBuilderAttributeInterface { public function __construct( /** @@ -44,7 +44,7 @@ public function __construct( /** * {@inheritdoc} */ - public function applyOnFormBuilder(AnnotationForm $form, FormBuilderInterface $builder): void + public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void { $builder->generates($this->className); } diff --git a/tests/Aggregate/ArrayConstraintTest.php b/tests/Aggregate/ArrayConstraintTest.php index e372c84..527930b 100644 --- a/tests/Aggregate/ArrayConstraintTest.php +++ b/tests/Aggregate/ArrayConstraintTest.php @@ -1,10 +1,10 @@ 'Not unique'])] public ArrayElement $values; }; diff --git a/tests/Aggregate/CountTest.php b/tests/Aggregate/CountTest.php index 2b1d9b9..b67451d 100644 --- a/tests/Aggregate/CountTest.php +++ b/tests/Aggregate/CountTest.php @@ -1,10 +1,10 @@ Date: Fri, 3 Dec 2021 10:57:14 +0100 Subject: [PATCH 05/40] Improve type coverage --- composer.json | 2 +- psalm.xml | 3 +- src/Aggregate/Count.php | 2 + src/Aggregate/ElementType.php | 2 +- src/AttributeForm.php | 70 +++++++++++++++++--------- src/Button/Groups.php | 1 + src/Child/CallbackModelTransformer.php | 6 +++ src/Child/Configure.php | 2 + src/Child/Dependencies.php | 1 + src/Constraint/CustomConstraint.php | 2 + src/Constraint/Satisfy.php | 2 +- src/Element/CallbackTransformer.php | 6 +++ src/Element/Choices.php | 5 +- src/Element/TransformerError.php | 2 + 14 files changed, 76 insertions(+), 30 deletions(-) diff --git a/composer.json b/composer.json index f46ddf2..b92de9c 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ }, "require-dev": { "phpunit/phpunit": "~9.5", - "vimeo/psalm": "~4.13.1" + "vimeo/psalm": "dev-master" }, "scripts": { "tests": "phpunit", diff --git a/psalm.xml b/psalm.xml index fe8c94d..3240886 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,7 +1,6 @@ - + * + * @psalm-suppress PropertyNotSetInConstructor */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Count extends CountConstraint implements ChildBuilderAttributeInterface diff --git a/src/Aggregate/ElementType.php b/src/Aggregate/ElementType.php index b015ab9..9581ebb 100644 --- a/src/Aggregate/ElementType.php +++ b/src/Aggregate/ElementType.php @@ -54,7 +54,7 @@ public function __construct( * The element configuration method name * This method must be defined on the form class, and with public visibility * - * @var string|null + * @var literal-string|null */ public ?string $configurator = null ) {} diff --git a/src/AttributeForm.php b/src/AttributeForm.php index 022ed7b..9102a6b 100644 --- a/src/AttributeForm.php +++ b/src/AttributeForm.php @@ -13,6 +13,7 @@ use Bdf\Form\PropertyAccess\ExtractorInterface; use Bdf\Form\PropertyAccess\HydratorInterface; use Bdf\Form\Transformer\TransformerInterface; +use ReflectionAttribute; use ReflectionClass; use ReflectionNamedType; use Symfony\Component\Validator\Constraint; @@ -23,12 +24,12 @@ abstract class AttributeForm extends CustomForm { /** - * @var array + * @var array */ private array $elementProperties = []; /** - * @var array + * @var array */ private array $buttonProperties = []; @@ -40,14 +41,15 @@ protected function configure(FormBuilderInterface $builder): void // @todo extraire dans une "attribute processor" for ($reflection = new ReflectionClass($this); $reflection->getName() !== AttributeForm::class; $reflection = $reflection->getParentClass()) { - foreach ($reflection->getAttributes() as $attribute) { - if (is_subclass_of($attribute->getName(), FormBuilderAttributeInterface::class)) { - $attribute->newInstance()->applyOnFormBuilder($this, $builder); - } + foreach ($reflection->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->applyOnFormBuilder($this, $builder); } foreach ($reflection->getProperties() as $property) { - if (!$property->hasType() || isset($this->elementProperties[$property->getName()]) || isset($this->buttonProperties[$property->getName()])) { + /** @var non-empty-string $name */ + $name = $property->getName(); + + if (!$property->hasType() || isset($this->elementProperties[$name]) || isset($this->buttonProperties[$name])) { continue; } @@ -60,14 +62,12 @@ protected function configure(FormBuilderInterface $builder): void // @todo is_subclass_of($elementType, ButtonInterface::class) ? if ($elementType === ButtonInterface::class) { $property->setAccessible(true); - $this->buttonProperties[$property->getName()] = $property; + $this->buttonProperties[$name] = $property; - $submitBuilder = $builder->submit($property->getName()); + $submitBuilder = $builder->submit($name); - foreach ($property->getAttributes() as $attribute) { - match (true) { - is_subclass_of($attribute->getName(), ButtonBuilderAttributeInterface::class) => $attribute->newInstance()->applyOnButtonBuilder($this, $submitBuilder), - }; + foreach ($property->getAttributes(ButtonBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->applyOnButtonBuilder($this, $submitBuilder); } continue; @@ -78,20 +78,42 @@ protected function configure(FormBuilderInterface $builder): void } $property->setAccessible(true); - $this->elementProperties[$property->getName()] = $property; + $this->elementProperties[$name] = $property; + + $elementBuilder = $builder->add($name, $elementType); + + foreach ($property->getAttributes(ChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->applyOnChildBuilder($this, $elementBuilder); + } + + foreach ($property->getAttributes(Constraint::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + $elementBuilder->satisfy($attribute->newInstance()); + } + } - $elementBuilder = $builder->add($property->getName(), $elementType); + foreach ($property->getAttributes(FilterInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + $elementBuilder->filter($attribute->newInstance()); + } + } + + foreach ($property->getAttributes(TransformerInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + $elementBuilder->transformer($attribute->newInstance()); + } + } - foreach ($property->getAttributes() as $attribute) { - match (true) { - is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class) => $attribute->newInstance()->applyOnChildBuilder($this, $elementBuilder), + foreach ($property->getAttributes(HydratorInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + $elementBuilder->hydrator($attribute->newInstance()); + } + } - is_subclass_of($attribute->getName(), Constraint::class) => $elementBuilder->satisfy($attribute->newInstance()), - is_subclass_of($attribute->getName(), FilterInterface::class) => $elementBuilder->filter($attribute->newInstance()), - is_subclass_of($attribute->getName(), TransformerInterface::class) => $elementBuilder->transformer($attribute->newInstance()), - is_subclass_of($attribute->getName(), HydratorInterface::class) => $elementBuilder->hydrator($attribute->newInstance()), - is_subclass_of($attribute->getName(), ExtractorInterface::class) => $elementBuilder->extractor($attribute->newInstance()), - }; + foreach ($property->getAttributes(ExtractorInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + $elementBuilder->extractor($attribute->newInstance()); + } } } } diff --git a/src/Button/Groups.php b/src/Button/Groups.php index 519cf21..df77c51 100644 --- a/src/Button/Groups.php +++ b/src/Button/Groups.php @@ -41,6 +41,7 @@ final class Groups implements ButtonBuilderAttributeInterface /** * @param string ...$groups List of validation groups + * @no-named-arguments */ public function __construct(string ...$groups) { diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index 235d978..ddbe829 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -69,16 +69,22 @@ public function __construct( /** * Method name use to define the unified transformer method * If defined, the other parameters will be ignored + * + * @var literal-string|null */ public ?string $callback = null, /** * Method name use to define the transformation process from input value to the entity + * + * @var literal-string|null */ public ?string $toEntity = null, /** * Method name use to define the transformation process from entity value to input + * + * @var literal-string|null */ public ?string $toInput = null, ) {} diff --git a/src/Child/Configure.php b/src/Child/Configure.php index 8e85a0a..9bb6e50 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -35,6 +35,8 @@ public function __construct( /** * The method name to use as configurator * The method should follow the prototype `public function (ChildBuilderInterface $builder): void` + * + * @var literal-string */ public string $callback ) {} diff --git a/src/Child/Dependencies.php b/src/Child/Dependencies.php index b41e7b6..f397647 100644 --- a/src/Child/Dependencies.php +++ b/src/Child/Dependencies.php @@ -38,6 +38,7 @@ final class Dependencies implements ChildBuilderAttributeInterface /** * @param string ...$dependencies List of inputs names + * @no-named-arguments */ public function __construct(string ...$dependencies) { diff --git a/src/Constraint/CustomConstraint.php b/src/Constraint/CustomConstraint.php index 00dfff5..08bb5ec 100644 --- a/src/Constraint/CustomConstraint.php +++ b/src/Constraint/CustomConstraint.php @@ -53,6 +53,8 @@ public function __construct( * - Return false for invalid input, with the default error message (or the declared one) * - Return string for a custom error message * - Return array with error message and code + * + * @var literal-string */ public string $methodName, diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index 9248356..1fca024 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -44,7 +44,7 @@ public function __construct( /** * Constraint's constructor options * - * @var mixed|null + * @var array|null|string */ public mixed $options = null ) {} diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index 07f54d2..1521e42 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -70,16 +70,22 @@ public function __construct( /** * Method name use to define the unified transformer method * If defined, the other parameters will be ignored + * + * @var literal-string|null */ public ?string $callback = null, /** * Method name use to define the transformation process from http value to the input + * + * @var literal-string|null */ public ?string $fromHttp = null, /** * Method name use to define the transformation process from input value to http format + * + * @var literal-string|null */ public ?string $toHttp = null, ) {} diff --git a/src/Element/Choices.php b/src/Element/Choices.php index ee793c4..933c7c8 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -10,6 +10,7 @@ use Bdf\Form\Choice\ArrayChoice; use Bdf\Form\Choice\Choiceable; use Bdf\Form\Choice\ChoiceBuilderTrait; +use Bdf\Form\Choice\ChoiceInterface; use Bdf\Form\Choice\LazzyChoice; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Leaf\StringElementBuilder; @@ -73,6 +74,8 @@ public function __construct( * * If the value is an array, the key will be used as label (displayed value), and the value as real value * The label is not required. + * + * @var literal-string|array */ public string|array $choices, @@ -106,7 +109,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ // Q&D fix for psalm because it does not recognize trait as type /** @var StringElementBuilder $builder */ $builder->choices( - is_string($this->choices) ? fn() => $form->{$this->choices}() : $this->choices, + is_string($this->choices) ? new LazzyChoice([$form, $this->choices]) : $this->choices, $options ); } diff --git a/src/Element/TransformerError.php b/src/Element/TransformerError.php index 089086c..6a196cd 100644 --- a/src/Element/TransformerError.php +++ b/src/Element/TransformerError.php @@ -58,6 +58,8 @@ public function __construct( * * If the method return false, the exception will be ignored * Else, the method should fill `TransformerExceptionConstraint` with error message and code to provide the custom error + * + * @var literal-string|null */ public ?string $validationCallback = null, ) {} From 52f77480c864873c03febd4e7254463c6f8a2d28 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 3 Dec 2021 11:15:26 +0100 Subject: [PATCH 06/40] Add infection and phpcs --- .github/workflows/php.yml | 19 +++++++++++-------- .gitignore | 2 ++ composer.json | 6 ++++-- infection.json | 14 ++++++++++++++ src/Aggregate/ArrayConstraint.php | 4 ++-- src/Aggregate/ElementType.php | 4 ++-- src/Button/Value.php | 3 ++- src/Child/CallbackModelTransformer.php | 12 ++++++------ src/Child/Configure.php | 3 ++- src/Child/DefaultValue.php | 3 ++- src/Child/GetSet.php | 3 ++- src/Child/ModelTransformer.php | 4 ++-- src/Constraint/CustomConstraint.php | 4 ++-- src/Constraint/Satisfy.php | 4 ++-- src/Element/CallbackTransformer.php | 12 ++++++------ src/Element/Choices.php | 5 ++--- src/Element/IgnoreTransformerException.php | 3 ++- src/Element/Transformer.php | 4 ++-- src/Element/TransformerError.php | 5 ++--- src/Form/CallbackGenerator.php | 3 ++- src/Form/Generates.php | 3 ++- 21 files changed, 73 insertions(+), 47 deletions(-) create mode 100644 infection.json diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index c8b0363..5fe0bc0 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -61,9 +61,9 @@ jobs: php-version: 8.0 extensions: json ini-values: date.timezone=Europe/Paris -# -# - name: Install Infection -# run: composer global require infection/infection + + - name: Install Infection + run: composer global require infection/infection - name: Validate composer.json and composer.lock run: composer validate --strict @@ -73,8 +73,11 @@ jobs: - name: Run type coverage run: composer run-script psalm -# -# - name: Run Infection -# run: | -# git fetch --depth=1 origin $GITHUB_BASE_REF -# ~/.composer/vendor/bin/infection --logger-github --git-diff-filter=AM + + - name: Run check style + run: composer run-script phpcs + + - name: Run Infection + run: | + git fetch --depth=1 origin $GITHUB_BASE_REF + ~/.composer/vendor/bin/infection --logger-github --git-diff-filter=AM diff --git a/.gitignore b/.gitignore index 97a6a13..673c59e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ vendor/ composer.lock .phpunit.result.cache +infection.log + diff --git a/composer.json b/composer.json index b92de9c..b3a7427 100644 --- a/composer.json +++ b/composer.json @@ -27,11 +27,13 @@ }, "require-dev": { "phpunit/phpunit": "~9.5", - "vimeo/psalm": "dev-master" + "vimeo/psalm": "dev-master", + "squizlabs/php_codesniffer": "~3.6.1" }, "scripts": { "tests": "phpunit", "tests-with-coverage": "phpunit --coverage-clover coverage.xml", - "psalm": "psalm --shepherd" + "psalm": "psalm --shepherd", + "phpcs": "phpcs -n --standard=psr12 --tab-width=4 src/" } } diff --git a/infection.json b/infection.json new file mode 100644 index 0000000..7af8d9d --- /dev/null +++ b/infection.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/infection/infection/0.25.3/resources/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "infection.log" + }, + "mutators": { + "@default": true + } +} diff --git a/src/Aggregate/ArrayConstraint.php b/src/Aggregate/ArrayConstraint.php index 608aa1a..0187adc 100644 --- a/src/Aggregate/ArrayConstraint.php +++ b/src/Aggregate/ArrayConstraint.php @@ -45,14 +45,14 @@ public function __construct( * @var class-string */ public string $constraint, - /** * Constraint's constructor options * * @var mixed|null */ public mixed $options = null - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Aggregate/ElementType.php b/src/Aggregate/ElementType.php index 9581ebb..ee7c4db 100644 --- a/src/Aggregate/ElementType.php +++ b/src/Aggregate/ElementType.php @@ -49,7 +49,6 @@ public function __construct( * @var class-string */ public string $elementType, - /** * The element configuration method name * This method must be defined on the form class, and with public visibility @@ -57,7 +56,8 @@ public function __construct( * @var literal-string|null */ public ?string $configurator = null - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Button/Value.php b/src/Button/Value.php index df9ca08..75fe3f2 100644 --- a/src/Button/Value.php +++ b/src/Button/Value.php @@ -35,7 +35,8 @@ public function __construct( * The button HTTP value */ public string $value, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index ddbe829..1f25f63 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -73,21 +73,20 @@ public function __construct( * @var literal-string|null */ public ?string $callback = null, - /** * Method name use to define the transformation process from input value to the entity * * @var literal-string|null */ public ?string $toEntity = null, - /** * Method name use to define the transformation process from entity value to input * * @var literal-string|null */ public ?string $toInput = null, - ) {} + ) { + } /** * {@inheritdoc} @@ -99,11 +98,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ return; } - $builder->modelTransformer(new class($form, $this) implements TransformerInterface { + $builder->modelTransformer(new class ($form, $this) implements TransformerInterface { public function __construct( - private AttributeForm $form, + private AttributeForm $form, private CallbackModelTransformer $attribute, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Child/Configure.php b/src/Child/Configure.php index 9bb6e50..0d3736b 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -39,7 +39,8 @@ public function __construct( * @var literal-string */ public string $callback - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Child/DefaultValue.php b/src/Child/DefaultValue.php index 1a51f84..02574e3 100644 --- a/src/Child/DefaultValue.php +++ b/src/Child/DefaultValue.php @@ -34,7 +34,8 @@ final class DefaultValue implements ChildBuilderAttributeInterface { public function __construct( public mixed $default - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Child/GetSet.php b/src/Child/GetSet.php index 3d8ab0e..f7cd76f 100644 --- a/src/Child/GetSet.php +++ b/src/Child/GetSet.php @@ -47,7 +47,8 @@ public function __construct( * If not provided, the input name will be used as property name */ public ?string $propertyName = null, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Child/ModelTransformer.php b/src/Child/ModelTransformer.php index 744b78d..c363261 100644 --- a/src/Child/ModelTransformer.php +++ b/src/Child/ModelTransformer.php @@ -38,14 +38,14 @@ public function __construct( * @var class-string */ public string $transformerClass, - /** * Arguments to provide on the transformer constructor * * @var array */ public array $constructorArguments = [], - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Constraint/CustomConstraint.php b/src/Constraint/CustomConstraint.php index 08bb5ec..68a9615 100644 --- a/src/Constraint/CustomConstraint.php +++ b/src/Constraint/CustomConstraint.php @@ -57,7 +57,6 @@ public function __construct( * @var literal-string */ public string $methodName, - /** * The error message to use * This option is used only if the validator return false, in other cases, @@ -66,7 +65,8 @@ public function __construct( * @var string|null */ public ?string $message = null, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index 1fca024..5abe2e5 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -40,14 +40,14 @@ public function __construct( * @var class-string */ public string $constraint, - /** * Constraint's constructor options * * @var array|null|string */ public mixed $options = null - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index 1521e42..44969c2 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -74,21 +74,20 @@ public function __construct( * @var literal-string|null */ public ?string $callback = null, - /** * Method name use to define the transformation process from http value to the input * * @var literal-string|null */ public ?string $fromHttp = null, - /** * Method name use to define the transformation process from input value to http format * * @var literal-string|null */ public ?string $toHttp = null, - ) {} + ) { + } /** * {@inheritdoc} @@ -100,11 +99,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ return; } - $builder->transformer(new class($form, $this) implements TransformerInterface { + $builder->transformer(new class ($form, $this) implements TransformerInterface { public function __construct( - private AttributeForm $form, + private AttributeForm $form, private CallbackTransformer $attribute, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Element/Choices.php b/src/Element/Choices.php index 933c7c8..a8351ef 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -78,7 +78,6 @@ public function __construct( * @var literal-string|array */ public string|array $choices, - /** * The error message * If not provided, a default message will be used @@ -86,14 +85,14 @@ public function __construct( * @var string|null */ public ?string $message = null, - /** * Extra constraint options * * @var array */ public array $options = [], - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Element/IgnoreTransformerException.php b/src/Element/IgnoreTransformerException.php index d7b5e91..dc08627 100644 --- a/src/Element/IgnoreTransformerException.php +++ b/src/Element/IgnoreTransformerException.php @@ -43,7 +43,8 @@ public function __construct( * Ignore or not transformer errors */ public bool $ignore = true, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index 3965ecd..9d0fda2 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -41,14 +41,14 @@ public function __construct( * @var class-string */ public string $transformerClass, - /** * Arguments to provide on the transformer constructor * * @var array */ public array $constructorArguments = [], - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Element/TransformerError.php b/src/Element/TransformerError.php index 6a196cd..ced3b6f 100644 --- a/src/Element/TransformerError.php +++ b/src/Element/TransformerError.php @@ -44,12 +44,10 @@ public function __construct( * The error message to show when transformer fail */ public ?string $message = null, - /** * The error code to provide when transformer fail */ public ?string $code = null, - /** * Method name to use for validate the transformer exception * @@ -62,7 +60,8 @@ public function __construct( * @var literal-string|null */ public ?string $validationCallback = null, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Form/CallbackGenerator.php b/src/Form/CallbackGenerator.php index d88b2f2..9656dfb 100644 --- a/src/Form/CallbackGenerator.php +++ b/src/Form/CallbackGenerator.php @@ -43,7 +43,8 @@ public function __construct( * `public function (FormInterface $form): mixed` */ public string $callback, - ) {} + ) { + } /** * {@inheritdoc} diff --git a/src/Form/Generates.php b/src/Form/Generates.php index 8c8832f..9763d9c 100644 --- a/src/Form/Generates.php +++ b/src/Form/Generates.php @@ -39,7 +39,8 @@ public function __construct( * @var class-string */ public string $className, - ) {} + ) { + } /** * {@inheritdoc} From 3351e63fbe0b61a8f59eb38dfbdc9b3e9e3dd600 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 3 Dec 2021 16:17:08 +0100 Subject: [PATCH 07/40] Refactor attribute processor system --- .gitignore | 1 - src/AttributeForm.php | 132 ++++-------------- src/Element/Transformer.php | 1 + .../AttributesProcessorInterface.php | 24 ++++ .../ConfigureFormBuilderStrategy.php | 94 +++++++++++++ .../Element/ConstraintAttributeProcessor.php | 34 +++++ .../ElementAttributeProcessorInterface.php | 37 +++++ .../Element/ExtractorAttributeProcessor.php | 33 +++++ .../Element/FilterAttributeProcessor.php | 33 +++++ .../Element/HydratorAttributeProcessor.php | 33 +++++ .../Element/TransformerAttributeProcessor.php | 34 +++++ src/Processor/PostConfigureInterface.php | 24 ++++ .../PostConfigureReflectionSetProperties.php | 46 ++++++ src/Processor/ReflectionProcessor.php | 91 ++++++++++++ src/Processor/ReflectionStrategyInterface.php | 61 ++++++++ tests/Child/CallbackModelTransformerTest.php | 28 ++++ tests/Child/DependenciesTest.php | 30 ++++ tests/Constraint/SatisfyTest.php | 27 ++++ tests/Element/CallbackTransformerTest.php | 31 ++++ tests/Element/TransformerTest.php | 45 ++++++ .../ConfigureFormBuilderStrategyTest.php | 39 ++++++ .../ConstraintAttributeProcessorTest.php | 33 +++++ .../ExtractorAttributeProcessorTest.php | 22 +++ .../Element/FilterAttributeProcessorTest.php | 42 ++++++ .../HydratorAttributeProcessorTest.php | 22 +++ .../TransformerAttributeProcessorTest.php | 52 +++++++ tests/Processor/ReflectionProcessorTest.php | 80 +++++++++++ 27 files changed, 1025 insertions(+), 104 deletions(-) create mode 100644 src/Processor/AttributesProcessorInterface.php create mode 100644 src/Processor/ConfigureFormBuilderStrategy.php create mode 100644 src/Processor/Element/ConstraintAttributeProcessor.php create mode 100644 src/Processor/Element/ElementAttributeProcessorInterface.php create mode 100644 src/Processor/Element/ExtractorAttributeProcessor.php create mode 100644 src/Processor/Element/FilterAttributeProcessor.php create mode 100644 src/Processor/Element/HydratorAttributeProcessor.php create mode 100644 src/Processor/Element/TransformerAttributeProcessor.php create mode 100644 src/Processor/PostConfigureInterface.php create mode 100644 src/Processor/PostConfigureReflectionSetProperties.php create mode 100644 src/Processor/ReflectionProcessor.php create mode 100644 src/Processor/ReflectionStrategyInterface.php create mode 100644 tests/Child/DependenciesTest.php create mode 100644 tests/Constraint/SatisfyTest.php create mode 100644 tests/Element/TransformerTest.php create mode 100644 tests/Processor/ConfigureFormBuilderStrategyTest.php create mode 100644 tests/Processor/Element/ConstraintAttributeProcessorTest.php create mode 100644 tests/Processor/Element/ExtractorAttributeProcessorTest.php create mode 100644 tests/Processor/Element/FilterAttributeProcessorTest.php create mode 100644 tests/Processor/Element/HydratorAttributeProcessorTest.php create mode 100644 tests/Processor/Element/TransformerAttributeProcessorTest.php create mode 100644 tests/Processor/ReflectionProcessorTest.php diff --git a/.gitignore b/.gitignore index 673c59e..5e803e8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ vendor/ composer.lock .phpunit.result.cache infection.log - diff --git a/src/AttributeForm.php b/src/AttributeForm.php index 9102a6b..395e2c0 100644 --- a/src/AttributeForm.php +++ b/src/AttributeForm.php @@ -4,19 +4,11 @@ use Bdf\Form\Aggregate\FormBuilderInterface; use Bdf\Form\Aggregate\FormInterface; -use Bdf\Form\Attribute\Button\ButtonBuilderAttributeInterface; -use Bdf\Form\Attribute\Form\FormBuilderAttributeInterface; -use Bdf\Form\Button\ButtonInterface; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\ConfigureFormBuilderStrategy; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Custom\CustomForm; -use Bdf\Form\ElementInterface; -use Bdf\Form\Filter\FilterInterface; -use Bdf\Form\PropertyAccess\ExtractorInterface; -use Bdf\Form\PropertyAccess\HydratorInterface; -use Bdf\Form\Transformer\TransformerInterface; -use ReflectionAttribute; -use ReflectionClass; -use ReflectionNamedType; -use Symfony\Component\Validator\Constraint; /** * Utility class for declare a form using PHP 8 attributes and declare elements using typed properties @@ -24,99 +16,38 @@ abstract class AttributeForm extends CustomForm { /** - * @var array + * Implementation use to process attributes and properties + * and for configure the form builder + * + * @var AttributesProcessorInterface */ - private array $elementProperties = []; + private AttributesProcessorInterface $processor; /** - * @var array + * Action to perform after the form was built + * + * @var PostConfigureInterface|null */ - private array $buttonProperties = []; + private ?PostConfigureInterface $postConfigure = null; /** - * {@inheritdoc} + * @param FormBuilderInterface|null $builder The form builder using by CustomForm + * @param AttributesProcessorInterface|null $processor The attributes processor. + * By default, use ReflectionProcessor with ConfigureFormBuilderStrategy as strategy */ - protected function configure(FormBuilderInterface $builder): void + public function __construct(?FormBuilderInterface $builder = null, ?AttributesProcessorInterface $processor = null) { - // @todo extraire dans une "attribute processor" - - for ($reflection = new ReflectionClass($this); $reflection->getName() !== AttributeForm::class; $reflection = $reflection->getParentClass()) { - foreach ($reflection->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $attribute->newInstance()->applyOnFormBuilder($this, $builder); - } - - foreach ($reflection->getProperties() as $property) { - /** @var non-empty-string $name */ - $name = $property->getName(); - - if (!$property->hasType() || isset($this->elementProperties[$name]) || isset($this->buttonProperties[$name])) { - continue; - } - - if (!$property->getType() instanceof ReflectionNamedType) { - continue; - } - - $elementType = $property->getType()->getName(); - - // @todo is_subclass_of($elementType, ButtonInterface::class) ? - if ($elementType === ButtonInterface::class) { - $property->setAccessible(true); - $this->buttonProperties[$name] = $property; - - $submitBuilder = $builder->submit($name); - - foreach ($property->getAttributes(ButtonBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $attribute->newInstance()->applyOnButtonBuilder($this, $submitBuilder); - } - - continue; - } - - if (!is_subclass_of($elementType, ElementInterface::class)) { - continue; - } - - $property->setAccessible(true); - $this->elementProperties[$name] = $property; - - $elementBuilder = $builder->add($name, $elementType); + parent::__construct($builder); - foreach ($property->getAttributes(ChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $attribute->newInstance()->applyOnChildBuilder($this, $elementBuilder); - } - - foreach ($property->getAttributes(Constraint::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { - $elementBuilder->satisfy($attribute->newInstance()); - } - } - - foreach ($property->getAttributes(FilterInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { - $elementBuilder->filter($attribute->newInstance()); - } - } - - foreach ($property->getAttributes(TransformerInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { - $elementBuilder->transformer($attribute->newInstance()); - } - } - - foreach ($property->getAttributes(HydratorInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { - $elementBuilder->hydrator($attribute->newInstance()); - } - } + $this->processor = $processor ?? new ReflectionProcessor(new ConfigureFormBuilderStrategy()); + } - foreach ($property->getAttributes(ExtractorInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { - $elementBuilder->extractor($attribute->newInstance()); - } - } - } - } + /** + * {@inheritdoc} + */ + protected function configure(FormBuilderInterface $builder): void + { + $this->postConfigure = $this->processor->configureBuilder($this, $builder); } /** @@ -124,14 +55,9 @@ protected function configure(FormBuilderInterface $builder): void */ public function postConfigure(FormInterface $form): void { - foreach ($this->elementProperties as $name => $reflection) { - $reflection->setValue($this, $form[$name]->element()); + if ($this->postConfigure) { + $this->postConfigure->postConfigure($this, $form); + $this->postConfigure = null; } - - foreach ($this->buttonProperties as $name => $reflection) { - $reflection->setValue($this, $this->root()->button($name)); - } - - unset($this->elementProperties); } } diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index 9d0fda2..a9318fb 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -41,6 +41,7 @@ public function __construct( * @var class-string */ public string $transformerClass, + /** * Arguments to provide on the transformer constructor * diff --git a/src/Processor/AttributesProcessorInterface.php b/src/Processor/AttributesProcessorInterface.php new file mode 100644 index 0000000..fa410aa --- /dev/null +++ b/src/Processor/AttributesProcessorInterface.php @@ -0,0 +1,24 @@ + + */ + 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()); + } + + /** + * Register a new processor for element attributes + * + * @param ElementAttributeProcessorInterface $processor + * + * @return void + * + * @template T as object + */ + public function registerElementAttributeProcessor(ElementAttributeProcessorInterface $processor): void + { + $this->elementProcessors[] = $processor; + } + + /** + * {@inheritdoc} + */ + public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void + { + foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->applyOnFormBuilder($form, $builder); + } + } + + /** + * {@inheritdoc} + */ + public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void + { + $submitBuilder = $builder->submit($name); + + foreach ($property->getAttributes(ButtonBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->applyOnButtonBuilder($form, $submitBuilder); + } + } + + /** + * {@inheritdoc} + */ + public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void + { + $elementBuilder = $builder->add($name, $elementType); + + foreach ($property->getAttributes(ChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->applyOnChildBuilder($form, $elementBuilder); + } + + foreach ($this->elementProcessors as $configurator) { + foreach ($property->getAttributes($configurator->type(), ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + $configurator->process($elementBuilder, $attribute->newInstance()); + } + } + } + } +} diff --git a/src/Processor/Element/ConstraintAttributeProcessor.php b/src/Processor/Element/ConstraintAttributeProcessor.php new file mode 100644 index 0000000..270ca22 --- /dev/null +++ b/src/Processor/Element/ConstraintAttributeProcessor.php @@ -0,0 +1,34 @@ + + */ +final class ConstraintAttributeProcessor implements ElementAttributeProcessorInterface +{ + /** + * {@inheritdoc} + */ + public function type(): string + { + return Constraint::class; + } + + /** + * {@inheritdoc} + */ + public function process(ChildBuilderInterface $builder, object $attribute): void + { + $builder->satisfy($attribute); + } +} diff --git a/src/Processor/Element/ElementAttributeProcessorInterface.php b/src/Processor/Element/ElementAttributeProcessorInterface.php new file mode 100644 index 0000000..5df05d5 --- /dev/null +++ b/src/Processor/Element/ElementAttributeProcessorInterface.php @@ -0,0 +1,37 @@ + + */ + public function type(): string; + + /** + * Apply the attribute on the child builder + * + * @param ChildBuilderInterface<\Bdf\Form\ElementBuilderInterface> $builder The element builder + * @param T $attribute The attribute instance + * + * @return void + * + * @see \ReflectionAttribute::newInstance() $attribute is created using this method + */ + public function process(ChildBuilderInterface $builder, object $attribute): void; +} diff --git a/src/Processor/Element/ExtractorAttributeProcessor.php b/src/Processor/Element/ExtractorAttributeProcessor.php new file mode 100644 index 0000000..f5d5e03 --- /dev/null +++ b/src/Processor/Element/ExtractorAttributeProcessor.php @@ -0,0 +1,33 @@ + + */ +final class ExtractorAttributeProcessor implements ElementAttributeProcessorInterface +{ + /** + * {@inheritdoc} + */ + public function type(): string + { + return ExtractorInterface::class; + } + + /** + * {@inheritdoc} + */ + public function process(ChildBuilderInterface $builder, object $attribute): void + { + $builder->extractor($attribute); + } +} diff --git a/src/Processor/Element/FilterAttributeProcessor.php b/src/Processor/Element/FilterAttributeProcessor.php new file mode 100644 index 0000000..4457d77 --- /dev/null +++ b/src/Processor/Element/FilterAttributeProcessor.php @@ -0,0 +1,33 @@ + + */ +final class FilterAttributeProcessor implements ElementAttributeProcessorInterface +{ + /** + * {@inheritdoc} + */ + public function type(): string + { + return FilterInterface::class; + } + + /** + * {@inheritdoc} + */ + public function process(ChildBuilderInterface $builder, object $attribute): void + { + $builder->filter($attribute); + } +} diff --git a/src/Processor/Element/HydratorAttributeProcessor.php b/src/Processor/Element/HydratorAttributeProcessor.php new file mode 100644 index 0000000..4e719e9 --- /dev/null +++ b/src/Processor/Element/HydratorAttributeProcessor.php @@ -0,0 +1,33 @@ + + */ +final class HydratorAttributeProcessor implements ElementAttributeProcessorInterface +{ + /** + * {@inheritdoc} + */ + public function type(): string + { + return HydratorInterface::class; + } + + /** + * {@inheritdoc} + */ + public function process(ChildBuilderInterface $builder, object $attribute): void + { + $builder->hydrator($attribute); + } +} diff --git a/src/Processor/Element/TransformerAttributeProcessor.php b/src/Processor/Element/TransformerAttributeProcessor.php new file mode 100644 index 0000000..96ddfe1 --- /dev/null +++ b/src/Processor/Element/TransformerAttributeProcessor.php @@ -0,0 +1,34 @@ + + */ +final class TransformerAttributeProcessor implements ElementAttributeProcessorInterface +{ + /** + * {@inheritdoc} + */ + public function type(): string + { + return TransformerInterface::class; + } + + /** + * {@inheritdoc} + */ + public function process(ChildBuilderInterface $builder, object $attribute): void + { + $builder->transformer($attribute); + } +} diff --git a/src/Processor/PostConfigureInterface.php b/src/Processor/PostConfigureInterface.php new file mode 100644 index 0000000..8c0f953 --- /dev/null +++ b/src/Processor/PostConfigureInterface.php @@ -0,0 +1,24 @@ + + */ + private array $elementProperties, + + /** + * Properties which store form buttons + * The key is the button name, and value is the reflection property + * + * @var array + */ + private array $buttonProperties, + ) { + } + + /** + * {@inheritdoc} + */ + public function postConfigure(AttributeForm $form, FormInterface $inner): void + { + foreach ($this->elementProperties as $name => $reflection) { + $reflection->setValue($form, $inner[$name]->element()); + } + + foreach ($this->buttonProperties as $name => $reflection) { + $reflection->setValue($form, $form->root()->button($name)); + } + } +} diff --git a/src/Processor/ReflectionProcessor.php b/src/Processor/ReflectionProcessor.php new file mode 100644 index 0000000..f30e9e8 --- /dev/null +++ b/src/Processor/ReflectionProcessor.php @@ -0,0 +1,91 @@ +strategy = $strategy; + } + + /** + * {@inheritdoc} + */ + public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $elementProperties = []; + $buttonProperties = []; + + foreach ($this->iterateClassHierarchy($form) as $formClass) { + $this->strategy->onFormClass($formClass, $form, $builder); + + foreach ($formClass->getProperties() as $property) { + /** @var non-empty-string $name */ + $name = $property->getName(); + + if ( + !$property->hasType() + || !$property->getType() instanceof ReflectionNamedType + || isset($elementProperties[$name]) + || isset($buttonProperties[$name]) + ) { + continue; + } + + $elementType = $property->getType()->getName(); + $property->setAccessible(true); + + if ($elementType === ButtonInterface::class) { + $buttonProperties[$name] = $property; + $this->strategy->onButtonProperty($property, $name, $form, $builder); + } elseif (is_subclass_of($elementType, ElementInterface::class)) { + $elementProperties[$name] = $property; + $this->strategy->onElementProperty($property, $name, $elementType, $form, $builder); + } + } + } + + return new PostConfigureReflectionSetProperties($elementProperties, $buttonProperties); + } + + /** + * 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 + * + * @return iterable> + * + * @psalm-suppress MoreSpecificReturnType + */ + private function iterateClassHierarchy(AttributeForm $form): iterable + { + for ($reflection = new ReflectionClass($form); $reflection->getName() !== AttributeForm::class; $reflection = $reflection->getParentClass()) { + yield $reflection; + } + } +} diff --git a/src/Processor/ReflectionStrategyInterface.php b/src/Processor/ReflectionStrategyInterface.php new file mode 100644 index 0000000..e128a86 --- /dev/null +++ b/src/Processor/ReflectionStrategyInterface.php @@ -0,0 +1,61 @@ + $formClass Form class to use + * @param AttributeForm $form The current form instance + * @param FormBuilderInterface $builder Builder to configure + * + * @return void + */ + public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void; + + /** + * Configure a button 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 button name + * @param AttributeForm $form The current form instance + * @param FormBuilderInterface $builder Builder to configure + * + * @return void + */ + public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): 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 FormBuilderInterface $builder Builder to configure + * + * @return void + */ + public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void; +} diff --git a/tests/Child/CallbackModelTransformerTest.php b/tests/Child/CallbackModelTransformerTest.php index 2ce4404..8122fc6 100644 --- a/tests/Child/CallbackModelTransformerTest.php +++ b/tests/Child/CallbackModelTransformerTest.php @@ -47,4 +47,32 @@ public function bToInput($value, IntegerElement $input) $this->assertEquals('Hello World !', $form->a->value()); $this->assertEquals(10, $form->b->value()); } + + /** + * + */ + public function test_with_only_one_transformation_method() + { + $form = new class extends AttributeForm { + #[CallbackModelTransformer(toEntity: 't'), Getter, Setter] + public IntegerElement $foo; + #[CallbackModelTransformer(toInput: 't'), Getter, Setter] + public IntegerElement $bar; + + public function t($value, $input) + { + return $value + 1; + } + }; + + $form->submit(['foo' => '5', 'bar' => '5']); + $this->assertSame([ + 'foo' => 6, + 'bar' => 5 + ], $form->value()); + + $form->import(['foo' => 5, 'bar' => 5]); + $this->assertSame(5, $form->foo->value()); + $this->assertSame(6, $form->bar->value()); + } } diff --git a/tests/Child/DependenciesTest.php b/tests/Child/DependenciesTest.php new file mode 100644 index 0000000..a988d0a --- /dev/null +++ b/tests/Child/DependenciesTest.php @@ -0,0 +1,30 @@ +foo->value() . $value . $this->bar->value(); + } + }; + + $form->submit(['foo' => 'a', 'bar' => 'b', 'baz' => 'c']); + $this->assertSame('acb', $form->baz->value()); + } +} diff --git a/tests/Constraint/SatisfyTest.php b/tests/Constraint/SatisfyTest.php new file mode 100644 index 0000000..80192ac --- /dev/null +++ b/tests/Constraint/SatisfyTest.php @@ -0,0 +1,27 @@ + 3])] + public StringElement $foo; + }; + + $form->submit(['foo' => 'ab']); + $this->assertFalse($form->valid()); + $this->assertEquals(['foo' => 'This value is too short. It should have 3 characters or more.'], $form->error()->toArray()); + + $form->submit(['foo' => 'abc']); + $this->assertTrue($form->valid()); + } +} diff --git a/tests/Element/CallbackTransformerTest.php b/tests/Element/CallbackTransformerTest.php index 87e91e3..4ec3ff4 100644 --- a/tests/Element/CallbackTransformerTest.php +++ b/tests/Element/CallbackTransformerTest.php @@ -3,7 +3,9 @@ namespace Tests\Form\Attribute\Element; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Child\GetSet; use Bdf\Form\Attribute\Element\CallbackTransformer; +use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\StringElement; use PHPUnit\Framework\TestCase; @@ -47,4 +49,33 @@ public function outTransformer($value, StringElement $input) $this->assertEquals('["[\"a\",true]",false]', $view['foo']->value()); $this->assertEquals('["out","[\"in\",\"b\"]"]', $view['bar']->value()); } + + /** + * + */ + public function test_with_only_one_transformation_method() + { + $form = new class extends AttributeForm { + #[CallbackTransformer(fromHttp: 't'), GetSet] + public IntegerElement $foo; + #[CallbackTransformer(toHttp: 't'), GetSet] + public IntegerElement $bar; + + public function t($value, $input) + { + return $value + 1; + } + }; + + $form->submit(['foo' => '5', 'bar' => '5']); + $this->assertSame(6, $form->foo->value()); + $this->assertSame(5, $form->bar->value()); + + $form->foo->import(5); + $form->bar->import(5); + + $view = $form->view(); + $this->assertEquals(5, $view['foo']->value()); + $this->assertEquals(6, $view['bar']->value()); + } } diff --git a/tests/Element/TransformerTest.php b/tests/Element/TransformerTest.php new file mode 100644 index 0000000..3e87836 --- /dev/null +++ b/tests/Element/TransformerTest.php @@ -0,0 +1,45 @@ +submit(['foo' => '_']); + $this->assertEquals('A_', $form->foo->value()); + + $view = $form->view(); + $this->assertEquals('A_A', $view['foo']->value()); + } +} + +class ATransformer implements TransformerInterface +{ + public function __construct( + public string $c + ) { + } + + public function transformToHttp($value, ElementInterface $input) + { + return $value . $this->c; + } + + public function transformFromHttp($value, ElementInterface $input) + { + return $this->c . $value; + } +} diff --git a/tests/Processor/ConfigureFormBuilderStrategyTest.php b/tests/Processor/ConfigureFormBuilderStrategyTest.php new file mode 100644 index 0000000..49ce03b --- /dev/null +++ b/tests/Processor/ConfigureFormBuilderStrategyTest.php @@ -0,0 +1,39 @@ +registerElementAttributeProcessor(new class implements ElementAttributeProcessorInterface { + public function type(): string { return Foo::class; } + + public function process(ChildBuilderInterface $builder, object $attribute): void + { + $builder->default('Foo'); + } + }); + + $form = new class(null, new ReflectionProcessor($strategy)) extends AttributeForm { + #[Foo] + public StringElement $foo; + }; + + $form->submit([]); + + $this->assertEquals('Foo', $form->foo->value()); + } +} + +#[\Attribute] +class Foo {} diff --git a/tests/Processor/Element/ConstraintAttributeProcessorTest.php b/tests/Processor/Element/ConstraintAttributeProcessorTest.php new file mode 100644 index 0000000..39df5fe --- /dev/null +++ b/tests/Processor/Element/ConstraintAttributeProcessorTest.php @@ -0,0 +1,33 @@ +submit(['foo' => 'bar']); + + $this->assertFalse($form->valid()); + $this->assertEquals('This value is too short. It should have 5 characters or more.', $form->foo->error()->global()); + + $form->submit(['foo' => 'azerty']); + + $this->assertFalse($form->valid()); + $this->assertEquals('This value should not be equal to "azerty".', $form->foo->error()->global()); + + $form->submit(['foo' => 'aqwzsx']); + $this->assertTrue($form->valid()); + } +} diff --git a/tests/Processor/Element/ExtractorAttributeProcessorTest.php b/tests/Processor/Element/ExtractorAttributeProcessorTest.php new file mode 100644 index 0000000..dcec853 --- /dev/null +++ b/tests/Processor/Element/ExtractorAttributeProcessorTest.php @@ -0,0 +1,22 @@ +import(['bar' => 'azerty']); + $this->assertSame('azerty', $form->foo->value()); + } +} diff --git a/tests/Processor/Element/FilterAttributeProcessorTest.php b/tests/Processor/Element/FilterAttributeProcessorTest.php new file mode 100644 index 0000000..e53078b --- /dev/null +++ b/tests/Processor/Element/FilterAttributeProcessorTest.php @@ -0,0 +1,42 @@ +submit(['foo' => 'bar']); + $this->assertEquals('barAB', $form->foo->value()); + } +} + +#[Attribute(Attribute::TARGET_PROPERTY)] +class AFilter implements FilterInterface +{ + public function filter($value, ChildInterface $input, $default) + { + return $value . 'A'; + } +} + +#[Attribute(Attribute::TARGET_PROPERTY)] +class BFilter implements FilterInterface +{ + public function filter($value, ChildInterface $input, $default) + { + return $value . 'B'; + } +} diff --git a/tests/Processor/Element/HydratorAttributeProcessorTest.php b/tests/Processor/Element/HydratorAttributeProcessorTest.php new file mode 100644 index 0000000..379bd04 --- /dev/null +++ b/tests/Processor/Element/HydratorAttributeProcessorTest.php @@ -0,0 +1,22 @@ +submit(['foo' => 'azerty']); + $this->assertSame(['bar' => 'azerty'], $form->value()); + } +} diff --git a/tests/Processor/Element/TransformerAttributeProcessorTest.php b/tests/Processor/Element/TransformerAttributeProcessorTest.php new file mode 100644 index 0000000..e29c1ae --- /dev/null +++ b/tests/Processor/Element/TransformerAttributeProcessorTest.php @@ -0,0 +1,52 @@ +submit(['foo' => 'azerty']); + $this->assertEquals('azertyBA', $form->foo->value()); + } +} + +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class ATransformer implements TransformerInterface +{ + public function transformToHttp($value, ElementInterface $input) + { + // TODO: Implement transformToHttp() method. + } + + public function transformFromHttp($value, ElementInterface $input) + { + return $value . 'A'; + } +} + +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class BTransformer implements TransformerInterface +{ + public function transformToHttp($value, ElementInterface $input) + { + // TODO: Implement transformToHttp() method. + } + + public function transformFromHttp($value, ElementInterface $input) + { + return $value . 'B'; + } +} diff --git a/tests/Processor/ReflectionProcessorTest.php b/tests/Processor/ReflectionProcessorTest.php new file mode 100644 index 0000000..b9a59fe --- /dev/null +++ b/tests/Processor/ReflectionProcessorTest.php @@ -0,0 +1,80 @@ +createMock(ReflectionStrategyInterface::class); + $processor = new ReflectionProcessor($strategy); + + $form = new B(); + $builder = new FormBuilder(); + + $strategy->expects($this->exactly(2))->method('onFormClass') + ->withConsecutive( + [new \ReflectionClass(B::class), $form, $builder], + [new \ReflectionClass(A::class), $form, $builder], + ) + ; + + $processor->configureBuilder($form, $builder); + } + + public function test_should_not_configure_twice_same_element_property() + { + $strategy = $this->createMock(ReflectionStrategyInterface::class); + $processor = new ReflectionProcessor($strategy); + + $form = new B(); + $builder = new FormBuilder(); + + $strategy->expects($this->once())->method('onElementProperty') + ->with(new \ReflectionProperty(B::class, 'foo'), 'foo', StringElement::class, $form, $builder) + ; + + $processor->configureBuilder($form, $builder); + } + + public function test_should_not_configure_twice_same_button_property() + { + $strategy = $this->createMock(ReflectionStrategyInterface::class); + $processor = new ReflectionProcessor($strategy); + + $form = new B(); + $builder = new FormBuilder(); + + $strategy->expects($this->once())->method('onButtonProperty') + ->with(new \ReflectionProperty(B::class, 'btn'), 'btn', $form, $builder) + ; + + $processor->configureBuilder($form, $builder); + } +} + +class A extends AttributeForm +{ + public $withoutType; + public array $withNotObjectType; + + public StringElement $foo; + protected ButtonInterface $btn; + + public ButtonInterface|StringElement $withUnionType; + public \ArrayObject $withInvalidType; +} + +class B extends A +{ + public StringElement $foo; + protected ButtonInterface $btn; +} From 90a951ac4cfab6841a07639cd0f5e1a19b78e31b Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 6 Dec 2021 09:04:44 +0100 Subject: [PATCH 08/40] Fix check style --- src/Element/Transformer.php | 1 - src/Processor/PostConfigureReflectionSetProperties.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index a9318fb..9d0fda2 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -41,7 +41,6 @@ public function __construct( * @var class-string */ public string $transformerClass, - /** * Arguments to provide on the transformer constructor * diff --git a/src/Processor/PostConfigureReflectionSetProperties.php b/src/Processor/PostConfigureReflectionSetProperties.php index 8818b0b..87dfe26 100644 --- a/src/Processor/PostConfigureReflectionSetProperties.php +++ b/src/Processor/PostConfigureReflectionSetProperties.php @@ -19,7 +19,6 @@ public function __construct( * @var array */ private array $elementProperties, - /** * Properties which store form buttons * The key is the button name, and value is the reflection property From bf1aeffb71cf1d282539c5db8a783eee304ac595 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 8 Dec 2021 11:12:16 +0100 Subject: [PATCH 09/40] Add code generation --- composer.json | 3 +- src/Aggregate/ArrayConstraint.php | 13 + src/Aggregate/Count.php | 28 ++ src/Aggregate/ElementType.php | 17 ++ .../ButtonBuilderAttributeInterface.php | 17 +- src/Button/Groups.php | 10 + src/Button/Value.php | 10 + src/Child/CallbackModelTransformer.php | 57 ++++ src/Child/Configure.php | 10 + src/Child/DefaultValue.php | 10 + src/Child/Dependencies.php | 10 + src/Child/GetSet.php | 11 + src/Child/ModelTransformer.php | 12 + src/ChildBuilderAttributeInterface.php | 17 +- src/Constraint/CustomConstraint.php | 20 ++ src/Constraint/Satisfy.php | 12 + src/Element/CallbackTransformer.php | 47 ++++ src/Element/Choices.php | 24 ++ src/Element/IgnoreTransformerException.php | 10 + src/Element/Transformer.php | 12 + src/Element/TransformerError.php | 24 ++ src/Form/CallbackGenerator.php | 11 + src/Form/FormBuilderAttributeInterface.php | 14 + src/Form/Generates.php | 17 ++ .../AttributesProcessorGenerator.php | 89 ++++++ .../CodeGenerator/ClassGenerator.php | 147 ++++++++++ src/Processor/CompileAttributesProcessor.php | 111 ++++++++ .../ConfigureFormBuilderStrategy.php | 8 + .../Element/ConstraintAttributeProcessor.php | 14 + .../ElementAttributeProcessorInterface.php | 14 + .../Element/ExtractorAttributeProcessor.php | 12 + .../Element/FilterAttributeProcessor.php | 14 + .../Element/HydratorAttributeProcessor.php | 14 + .../Element/TransformerAttributeProcessor.php | 14 + .../GenerateConfiguratorStrategy.php | 181 ++++++++++++ src/Processor/ReflectionProcessor.php | 2 +- src/Processor/ReflectionStrategyInterface.php | 8 + tests/Aggregate/ArrayConstraintTest.php | 59 +++- tests/Aggregate/CountTest.php | 59 +++- tests/Aggregate/ElementTypeTest.php | 78 +++++- tests/Button/GroupsTest.php | 60 +++- tests/Button/ValueTest.php | 60 +++- tests/Child/CallbackModelTransformerTest.php | 127 ++++++++- tests/Child/ConfigureTest.php | 61 +++- tests/Child/DefaultValueTest.php | 62 ++++- tests/Child/DependenciesTest.php | 72 ++++- tests/Child/GetSetTest.php | 66 ++++- tests/Child/ModelTransformerTest.php | 78 +++++- tests/Constraint/CustomConstraintTest.php | 73 ++++- tests/Constraint/SatisfyTest.php | 55 +++- tests/Element/CallbackTransformerTest.php | 105 ++++++- tests/Element/ChoicesTest.php | 74 ++++- .../IgnoreTransformerExceptionTest.php | 67 ++++- tests/Element/TransformerErrorTest.php | 77 +++++- tests/Element/TransformerTest.php | 55 +++- tests/Form/CallbackGeneratorTest.php | 56 +++- tests/Form/GeneratesTest.php | 53 +++- tests/FunctionalTest.php | 108 ++++++-- .../AttributesProcessorGeneratorTest.php | 121 ++++++++ .../CodeGenerator/ClassGeneratorTest.php | 139 ++++++++++ .../CompileAttributesProcessorTest.php | 259 +++++++++++++++++ .../ConfigureFormBuilderStrategyTest.php | 9 +- .../ConstraintAttributeProcessorTest.php | 57 +++- .../ExtractorAttributeProcessorTest.php | 55 +++- .../Element/FilterAttributeProcessorTest.php | 58 +++- .../HydratorAttributeProcessorTest.php | 54 +++- .../TransformerAttributeProcessorTest.php | 57 +++- .../GenerateConfiguratorStrategyTest.php | 261 ++++++++++++++++++ tests/Processor/ReflectionProcessorTest.php | 8 +- tests/TestCase.php | 37 +++ 70 files changed, 3533 insertions(+), 131 deletions(-) create mode 100644 src/Processor/CodeGenerator/AttributesProcessorGenerator.php create mode 100644 src/Processor/CodeGenerator/ClassGenerator.php create mode 100644 src/Processor/CompileAttributesProcessor.php create mode 100644 src/Processor/GenerateConfiguratorStrategy.php create mode 100644 tests/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php create mode 100644 tests/Processor/CodeGenerator/ClassGeneratorTest.php create mode 100644 tests/Processor/CompileAttributesProcessorTest.php create mode 100644 tests/Processor/GenerateConfiguratorStrategyTest.php create mode 100644 tests/TestCase.php diff --git a/composer.json b/composer.json index b3a7427..924a01d 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "minimum-stability": "dev", "require": { "php": "8.0.*", - "b2pweb/bdf-form": "dev-prepare-attributes" + "b2pweb/bdf-form": "dev-prepare-attributes", + "nette/php-generator": "~3.6" }, "require-dev": { "phpunit/phpunit": "~9.5", diff --git a/src/Aggregate/ArrayConstraint.php b/src/Aggregate/ArrayConstraint.php index 0187adc..e77ba1d 100644 --- a/src/Aggregate/ArrayConstraint.php +++ b/src/Aggregate/ArrayConstraint.php @@ -7,7 +7,10 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Constraint\Satisfy; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; +use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; /** @@ -61,4 +64,14 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->arrayConstraint($this->constraint, $this->options); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $constraint = $generator->useAndSimplifyType($this->constraint); + + $generator->line('$?->arrayConstraint(?::class, ?);', [$name, new Literal($constraint), $this->options]); + } } diff --git a/src/Aggregate/Count.php b/src/Aggregate/Count.php index c276213..a303471 100644 --- a/src/Aggregate/Count.php +++ b/src/Aggregate/Count.php @@ -6,6 +6,7 @@ use Bdf\Form\Aggregate\ArrayElementBuilder; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; use Symfony\Component\Validator\Constraints\Count as CountConstraint; @@ -53,4 +54,31 @@ public function validatedBy(): string { return CountConstraint::class . 'Validator'; } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $defaultParameters = get_class_vars(CountConstraint::class); + /** @var array{ + * minMessage?: string, + * maxMessage?: string, + * exactMessage?: string, + * divisibleByMessage?: string, + * min?: int|null, + * max?: int|null, + * divisibleBy?: int|null, + * } $parameters + */ + $parameters = get_object_vars($this); + + foreach ($parameters as $paramName => $value) { + if (!array_key_exists($paramName, $defaultParameters) || $value === $defaultParameters[$paramName]) { + unset($parameters[$paramName]); + } + } + + $generator->line('$?->arrayConstraint(?);', [$name, $generator->new(CountConstraint::class, $parameters)]); + } } diff --git a/src/Aggregate/ElementType.php b/src/Aggregate/ElementType.php index ee7c4db..50be39b 100644 --- a/src/Aggregate/ElementType.php +++ b/src/Aggregate/ElementType.php @@ -6,8 +6,11 @@ use Bdf\Form\Aggregate\ArrayElementBuilder; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementInterface; +use Nette\PhpGenerator\Literal; /** * Attribute for define the array element type @@ -67,4 +70,18 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $configurator = $this->configurator ? [$form, $this->configurator] : null; $builder->element($this->elementType, $configurator); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $elementType = new Literal($generator->useAndSimplifyType($this->elementType)); + + if ($this->configurator) { + $generator->line('$?->element(?::class, [$form, ?]);', [$name, $elementType, $this->configurator]); + } else { + $generator->line('$?->element(?::class);', [$name, $elementType]); + } + } } diff --git a/src/Button/ButtonBuilderAttributeInterface.php b/src/Button/ButtonBuilderAttributeInterface.php index ea183cd..3dab8c1 100644 --- a/src/Button/ButtonBuilderAttributeInterface.php +++ b/src/Button/ButtonBuilderAttributeInterface.php @@ -3,6 +3,8 @@ namespace Bdf\Form\Attribute\Button; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Button\ButtonBuilderInterface; /** @@ -16,8 +18,19 @@ interface ButtonBuilderAttributeInterface /** * Configure the button builder * - * @param AttributeForm $form - * @param ButtonBuilderInterface $builder + * @param AttributeForm $form The current form instance + * @param ButtonBuilderInterface $builder Builder to configure */ public function applyOnButtonBuilder(AttributeForm $form, 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 + * + * @return void + */ + public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void; } diff --git a/src/Button/Groups.php b/src/Button/Groups.php index df77c51..9ee37cd 100644 --- a/src/Button/Groups.php +++ b/src/Button/Groups.php @@ -4,6 +4,8 @@ use Attribute; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Button\ButtonBuilderInterface; use Bdf\Form\Button\ButtonInterface; use Bdf\Form\RootElementInterface; @@ -55,4 +57,12 @@ public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface { $builder->groups($this->groups); } + + /** + * {@inheritdoc} + */ + public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line(' ->groups(?)', [$this->groups]); + } } diff --git a/src/Button/Value.php b/src/Button/Value.php index 75fe3f2..62a88bb 100644 --- a/src/Button/Value.php +++ b/src/Button/Value.php @@ -4,6 +4,8 @@ use Attribute; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Button\ButtonBuilderInterface; /** @@ -45,4 +47,12 @@ public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface { $builder->value($this->value); } + + /** + * {@inheritdoc} + */ + public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line(' ->value(?)', [$this->value]); + } } diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index 1f25f63..183aae2 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -6,9 +6,15 @@ 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\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; /** * Add a model transformer on the child element, by using method @@ -130,4 +136,55 @@ public function transformFromHttp($value, ElementInterface $input) } }); } + + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + // @todo refactor with CallbackTransformer + if ($this->callback) { + $generator->line('$?->modelTransformer([$form, ?]);', [$name, $this->callback]); + return; + } + + $generator->use(TransformerInterface::class); + $generator->use(ElementInterface::class); + + $transformer = new ClassType(); + $transformer->addImplement(TransformerInterface::class); + + $transformer->addProperty('form'); + + $constructor = $transformer->addMethod('__construct'); + $constructor->addParameter('form'); + $constructor->setBody('$this->form = $form;'); + + $toInput = Method::from([TransformerInterface::class, 'transformToHttp']); + $toEntity = Method::from([TransformerInterface::class, 'transformFromHttp']); + + $toInput->setComment('{@inheritdoc}'); + $toEntity->setComment('{@inheritdoc}'); + + $transformer->addMember($toInput); + $transformer->addMember($toEntity); + + if ($this->toInput) { + $toInput->setBody('return $this->form->?($value, $input);', [$this->toInput]); + } else { + $toInput->setBody('return $value;'); + } + + if ($this->toEntity) { + $toEntity->setBody('return $this->form->?($value, $input);', [$this->toEntity]); + } else { + $toEntity->setBody('return $value;'); + } + + $generator->line( + '$?->modelTransformer(new class ($form) ?);', + [$name, new Literal((new PsrPrinter())->printClass($transformer, $generator->namespace()))] + ); + } } diff --git a/src/Child/Configure.php b/src/Child/Configure.php index 0d3736b..c95370b 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -5,6 +5,8 @@ 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\ChildBuilderInterface; /** @@ -49,4 +51,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $form->{$this->callback}($builder); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$form->?($?);', [$this->callback, $name]); + } } diff --git a/src/Child/DefaultValue.php b/src/Child/DefaultValue.php index 02574e3..8bd1670 100644 --- a/src/Child/DefaultValue.php +++ b/src/Child/DefaultValue.php @@ -5,6 +5,8 @@ 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\ChildBuilderInterface; /** @@ -44,4 +46,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->default($this->default); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->default(?);', [$name, $this->default]); + } } diff --git a/src/Child/Dependencies.php b/src/Child/Dependencies.php index f397647..1390993 100644 --- a/src/Child/Dependencies.php +++ b/src/Child/Dependencies.php @@ -5,6 +5,8 @@ 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\ChildBuilderInterface; /** @@ -52,4 +54,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->depends(...$this->dependencies); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->depends(...?);', [$name, $this->dependencies]); + } } diff --git a/src/Child/GetSet.php b/src/Child/GetSet.php index f7cd76f..5c99ce7 100644 --- a/src/Child/GetSet.php +++ b/src/Child/GetSet.php @@ -5,6 +5,8 @@ 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\ChildBuilderInterface; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; @@ -57,4 +59,13 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->hydrator(new Setter($this->propertyName))->extractor(new Getter($this->propertyName)); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): 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/Child/ModelTransformer.php b/src/Child/ModelTransformer.php index c363261..d1e8531 100644 --- a/src/Child/ModelTransformer.php +++ b/src/Child/ModelTransformer.php @@ -5,8 +5,11 @@ 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\ChildBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; +use Nette\PhpGenerator\Literal; /** * Add a model transformer on the child, using a transformer class @@ -54,4 +57,13 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->modelTransformer(new $this->transformerClass(...$this->constructorArguments)); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $transformer = $generator->useAndSimplifyType($this->transformerClass); + $generator->line('$?->modelTransformer(new ?(...?));', [$name, new Literal($transformer), $this->constructorArguments]); + } } diff --git a/src/ChildBuilderAttributeInterface.php b/src/ChildBuilderAttributeInterface.php index 3f58efd..02aee07 100644 --- a/src/ChildBuilderAttributeInterface.php +++ b/src/ChildBuilderAttributeInterface.php @@ -2,6 +2,7 @@ namespace Bdf\Form\Attribute; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; /** @@ -17,8 +18,20 @@ interface ChildBuilderAttributeInterface /** * Configure the child builder * - * @param AttributeForm $form - * @param ChildBuilderInterface $builder + * @param AttributeForm $form The current form instance + * @param ChildBuilderInterface $builder The builder to configure */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void; + + /** + * Generate the code corresponding to the attribute + * The generated code must perform same action as `applyOnChildBuilder()` + * + * @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 + * + * @return void + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void; } diff --git a/src/Constraint/CustomConstraint.php b/src/Constraint/CustomConstraint.php index 68a9615..b28c8dd 100644 --- a/src/Constraint/CustomConstraint.php +++ b/src/Constraint/CustomConstraint.php @@ -5,9 +5,12 @@ 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\ChildBuilderInterface; use Bdf\Form\Constraint\Closure; use Bdf\Form\ElementBuilderInterface; +use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; /** @@ -37,6 +40,8 @@ * @see ElementBuilderInterface::satisfy() The called method * @see Constraint * @see Closure The used constraint + * + * @todo rename callback constraint */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class CustomConstraint implements ChildBuilderAttributeInterface @@ -81,4 +86,19 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $builder->satisfy($constraint); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->use(Closure::class, 'ClosureConstraint'); + + $parameters = $this->message + ? new Literal("['callback' => [\$form, ?], 'message' => ?]", [$this->methodName, $this->message]) + : new Literal('[$form, ?]', [$this->methodName]) + ; + + $generator->line('$?->satisfy(new ClosureConstraint(?));', [$name, $parameters]); + } } diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index 5abe2e5..8223916 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -5,7 +5,10 @@ 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\ChildBuilderInterface; +use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; /** @@ -56,4 +59,13 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->satisfy($this->constraint, $this->options); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $type = $generator->useAndSimplifyType($this->constraint); + $generator->line('$?->satisfy(?::class, ?);', [$name, new Literal($type), $this->options]); + } } diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index 44969c2..9f122bc 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -6,10 +6,18 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\CallbackModelTransformer; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\CodeGenerator\ClassGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\ElementInterface; use Bdf\Form\Transformer\TransformerInterface; +use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\Factory; +use Nette\PhpGenerator\Literal; +use Nette\PhpGenerator\Method; +use Nette\PhpGenerator\PsrPrinter; /** * Add a HTTP transformer on the child element, by using method @@ -131,4 +139,43 @@ public function transformFromHttp($value, ElementInterface $input) } }); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + if ($this->callback) { + $generator->line('$?->transformer([$form, ?]);', [$name, $this->callback]); + return; + } + + $transformer = $generator->anonymousClass(); + $transformer->implements(TransformerInterface::class); + + $transformer->class()->addProperty('form'); + $constructor = $transformer->class()->addMethod('__construct'); + $constructor->addParameter('form'); + $constructor->setBody('$this->form = $form;'); + + $toHttp = $transformer->implementsMethod(TransformerInterface::class, 'transformToHttp'); + $fromHttp = $transformer->implementsMethod(TransformerInterface::class, 'transformFromHttp'); + + if ($this->toHttp) { + $toHttp->setBody('return $this->form->?($value, $input);', [$this->toHttp]); + } else { + $toHttp->setBody('return $value;'); + } + + if ($this->fromHttp) { + $fromHttp->setBody('return $this->form->?($value, $input);', [$this->fromHttp]); + } else { + $fromHttp->setBody('return $value;'); + } + + $generator->line( + '$?->transformer(new class ($form) ?);', + [$name, new Literal($transformer->generateClass())] + ); + } } diff --git a/src/Element/Choices.php b/src/Element/Choices.php index a8351ef..e9d5153 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -6,6 +6,8 @@ use Bdf\Form\AbstractElementBuilder; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Choice\ArrayChoice; use Bdf\Form\Choice\Choiceable; @@ -14,6 +16,7 @@ use Bdf\Form\Choice\LazzyChoice; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Leaf\StringElementBuilder; +use Nette\PhpGenerator\Literal; /** * Define available values choice for the element @@ -112,4 +115,25 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $options ); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $options = $this->options; + + if ($this->message) { + $options['message'] = $options['multipleMessage'] = $this->message; + } + + if (is_string($this->choices)) { + $generator->use(LazzyChoice::class); + $choices = new Literal('new LazzyChoice([$form, ?])', [$this->choices]); + } else { + $choices = $this->choices; + } + + $generator->line('$?->choices(?, ?);', [$name, $choices, $options]); + } } diff --git a/src/Element/IgnoreTransformerException.php b/src/Element/IgnoreTransformerException.php index dc08627..25be0ef 100644 --- a/src/Element/IgnoreTransformerException.php +++ b/src/Element/IgnoreTransformerException.php @@ -6,6 +6,8 @@ use Bdf\Form\AbstractElementBuilder; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Util\ValidatorBuilderTrait; @@ -53,4 +55,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->ignoreTransformerException($this->ignore); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->ignoreTransformerException(?);', [$name, $this->ignore]); + } } diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index 9d0fda2..e6cb3b5 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -5,9 +5,12 @@ 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\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; +use Nette\PhpGenerator\Literal; /** * Add a transformer on the element, using a transformer class @@ -57,4 +60,13 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $builder->transformer(new $this->transformerClass(...$this->constructorArguments)); } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $transformer = $generator->useAndSimplifyType($this->transformerClass); + $generator->line('$?->transformer(new ?(...?));', [$name, new Literal($transformer), $this->constructorArguments]); + } } diff --git a/src/Element/TransformerError.php b/src/Element/TransformerError.php index ced3b6f..6eecb2d 100644 --- a/src/Element/TransformerError.php +++ b/src/Element/TransformerError.php @@ -6,6 +6,8 @@ use Bdf\Form\AbstractElementBuilder; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; @@ -80,4 +82,26 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $builder->transformerExceptionValidation([$form, $this->validationCallback]); } } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?', [$name]); + + if ($this->message) { + $generator->line(' ->transformerErrorMessage(?)', [$this->message]); + } + + if ($this->code) { + $generator->line(' ->transformerErrorCode(?)', [$this->code]); + } + + if ($this->validationCallback) { + $generator->line(' ->transformerExceptionValidation([$form, ?])', [$this->validationCallback]); + } + + $generator->line(';'); + } } diff --git a/src/Form/CallbackGenerator.php b/src/Form/CallbackGenerator.php index 9656dfb..0dd4842 100644 --- a/src/Form/CallbackGenerator.php +++ b/src/Form/CallbackGenerator.php @@ -6,6 +6,9 @@ use Bdf\Form\Aggregate\FormBuilderInterface; use Bdf\Form\Aggregate\Value\ValueGenerator; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Nette\PhpGenerator\Method; /** * Define the value generator of the form, using a callback method @@ -53,4 +56,12 @@ public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $bu { $builder->generates([$form, $this->callback]); } + + /** + * {@inheritdoc} + */ + public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$builder->generates([$this, ?]);', [$this->callback]); + } } diff --git a/src/Form/FormBuilderAttributeInterface.php b/src/Form/FormBuilderAttributeInterface.php index 061b079..240dba8 100644 --- a/src/Form/FormBuilderAttributeInterface.php +++ b/src/Form/FormBuilderAttributeInterface.php @@ -4,6 +4,9 @@ use Bdf\Form\Aggregate\FormBuilderInterface; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Nette\PhpGenerator\Method; /** * Attribute type for configure a form builder @@ -20,4 +23,15 @@ interface FormBuilderAttributeInterface * @param FormBuilderInterface $builder The form builder */ public function applyOnFormBuilder(AttributeForm $form, 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 + * + * @return void + */ + public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void; } diff --git a/src/Form/Generates.php b/src/Form/Generates.php index 9763d9c..3573beb 100644 --- a/src/Form/Generates.php +++ b/src/Form/Generates.php @@ -5,6 +5,10 @@ use Attribute; use Bdf\Form\Aggregate\FormBuilderInterface; 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; /** * Define the value generator of the form, using a class name @@ -49,4 +53,17 @@ public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $bu { $builder->generates($this->className); } + + /** + * {@inheritdoc} + */ + public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line( + '$builder->generates(?::class);', + [ + new Literal($generator->useAndSimplifyType($this->className)) + ] + ); + } } diff --git a/src/Processor/CodeGenerator/AttributesProcessorGenerator.php b/src/Processor/CodeGenerator/AttributesProcessorGenerator.php new file mode 100644 index 0000000..0d5801d --- /dev/null +++ b/src/Processor/CodeGenerator/AttributesProcessorGenerator.php @@ -0,0 +1,89 @@ +addClass(substr($className, $classNamePos + 1)) + ); + + $this->implements(AttributesProcessorInterface::class); + + $this->method = $this->implementsMethod(AttributesProcessorInterface::class, 'configureBuilder'); + } + + /** + * Add a line on the body method of "configureBuilder" + * + * @param string $line Line to add. Set empty string (default parameter) to simply add empty new line + * @param array $args Placeholder arguments + * + * @return self + */ + public function line(string $line = '', array $args = []): self + { + $this->method->addBody($line, $args); + + return $this; + } + + /** + * Create a new expression, with use class name + * + * @param class-string $className The class to create + * @param array $parameters The constructor parameters + * @param string|null $classAlias Class alias to use + * + * @return Literal + */ + public function new(string $className, array $parameters, ?string $classAlias = null): Literal + { + $className = $this->useAndSimplifyType($className, $classAlias); + + return new Literal('new ?(...?:)', [new Literal($className), $parameters]); + } + + /** + * Get utility for generate an anonymous class + * + * @return ClassGenerator + */ + public function anonymousClass(): ClassGenerator + { + return new ClassGenerator($this->namespace(), new ClassType(), $this->printer()); + } + + /** + * Print the class code + * + * @return string + */ + public function print(): string + { + return $this->printer()->printNamespace($this->namespace()); + } +} diff --git a/src/Processor/CodeGenerator/ClassGenerator.php b/src/Processor/CodeGenerator/ClassGenerator.php new file mode 100644 index 0000000..d00e0ff --- /dev/null +++ b/src/Processor/CodeGenerator/ClassGenerator.php @@ -0,0 +1,147 @@ +namespace = $namespace; + $this->class = $class; + $this->printer = $printer ?? new PsrPrinter(); + } + + /** + * Get the namespace object + * + * @return PhpNamespace + */ + final public function namespace(): PhpNamespace + { + return $this->namespace; + } + + /** + * Get the current class instance + * + * @return ClassType + */ + final public function class(): ClassType + { + return $this->class; + } + + /** + * Add an implemented interface on the class, and add use statement + * + * @param class-string $interface + * + * @return self + */ + final public function implements(string $interface): self + { + $this->class->addImplement($interface); + $this->namespace->addUse($interface); + + return $this; + } + + /** + * Implements a method of an interface, and auto-use all declared types (parameters and return type) + * + * @param class-string $interface The interface where the method is declared + * @param literal-string $methodName The method name to implements + * + * @return Method + */ + final public function implementsMethod(string $interface, string $methodName): Method + { + $method = Method::from([$interface, $methodName]); + $method->setComment('{@inheritdoc}'); + $method->setBody(''); // Ensure that the body is not null + + foreach ($method->getParameters() as $parameter) { + /** @var Type|null $type */ + $type = $parameter->getType(true); + + if ($type && $type->isClass()) { + /** @psalm-suppress PossiblyNullArgument */ + $this->namespace->addUse($type->getSingleName()); + } + } + + /** @var Type|null $returnType */ + $returnType = $method->getReturnType(true); + + if ($returnType && $returnType->isClass()) { + /** @psalm-suppress PossiblyNullArgument */ + $this->namespace->addUse($returnType->getSingleName()); + } + + $this->class->addMember($method); + + return $method; + } + + /** + * Add use statement and simplify it + * + * @param class-string $type Type to use and simplify + * @param string|null $alias Use alias + * + * @return string + */ + final public function useAndSimplifyType(string $type, ?string $alias = null): string + { + return $this->namespace->addUse($type, $alias)->simplifyType($type); + } + + /** + * Add use statement + * + * @param class-string $type Type to use + * @param string|null $alias Use alias + * + * @return self + */ + final public function use(string $type, ?string $alias = null): self + { + $this->namespace->addUse($type, $alias); + + return $this; + } + + /** + * Generate the class code + * + * @return string + */ + final public function generateClass(): string + { + return $this->printer->printClass($this->class, $this->namespace); + } + + /** + * Get the related printer instance + * + * @return Printer + */ + final public function printer(): Printer + { + return $this->printer; + } +} diff --git a/src/Processor/CompileAttributesProcessor.php b/src/Processor/CompileAttributesProcessor.php new file mode 100644 index 0000000..b64566c --- /dev/null +++ b/src/Processor/CompileAttributesProcessor.php @@ -0,0 +1,111 @@ +):non-empty-string + */ + private $fileNameResolver, + ) { + } + + /** + * {@inheritdoc} + */ + public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): PostConfigureInterface + { + /** @var class-string $className */ + $className = ($this->classNameResolver)($form); + + if (!class_exists($className)) { + $this->loadProcessor($className, $form, $builder); + } + + $generated = new $className(); + $generated->configureBuilder($form, $builder); + + return $generated; + } + + /** + * Try to load the processor from its file + * + * @param class-string $className Generated processor class name + * @param AttributeForm $form Form to build + * @param FormBuilderInterface $builder Builder to configure + * + * @return void + */ + private function loadProcessor(string $className, AttributeForm $form, FormBuilderInterface $builder): void + { + $fileName = ($this->fileNameResolver)($className); + + if (!file_exists($fileName)) { + $this->generateProcessor($fileName, $className, $form, $builder); + } + + require_once $fileName; + + if (!class_exists($className) || !is_subclass_of($className, AttributesProcessorInterface::class)) { + throw new LogicException('Invalid generated class "' . $className . '" in file "' . $fileName . '"'); + } + } + + /** + * 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 FormBuilderInterface $builder Builder to configure + * + * @return void + */ + private function generateProcessor(string $fileName, string $className, AttributeForm $form, FormBuilderInterface $builder): void + { + $generator = new GenerateConfiguratorStrategy($className); + $processor = new ReflectionProcessor($generator); + + $processor->configureBuilder($form, $builder); + + $code = $generator->code(); + + $dirname = dirname($fileName); + + if (!is_dir($dirname)) { + mkdir($dirname, 0777, true); + } + + file_put_contents($fileName, 'satisfy($attribute); } + + /** + * {@inheritdoc} + */ + public function generateCode(string $name, AttributesProcessorGenerator $generator, ReflectionAttribute $attribute): void + { + /** @var class-string $constraint */ + $constraint = $attribute->getName(); + $generator->line('$?->satisfy(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + } } diff --git a/src/Processor/Element/ElementAttributeProcessorInterface.php b/src/Processor/Element/ElementAttributeProcessorInterface.php index 5df05d5..6d1cc55 100644 --- a/src/Processor/Element/ElementAttributeProcessorInterface.php +++ b/src/Processor/Element/ElementAttributeProcessorInterface.php @@ -2,8 +2,10 @@ namespace Bdf\Form\Attribute\Processor\Element; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\ConfigureFormBuilderStrategy; use Bdf\Form\Child\ChildBuilderInterface; +use ReflectionAttribute; /** * Process an attribute type to configure the child builder @@ -34,4 +36,16 @@ public function type(): string; * @see \ReflectionAttribute::newInstance() $attribute is created using this method */ public function process(ChildBuilderInterface $builder, object $attribute): void; + + /** + * Generate the code corresponding to the attribute + * The generated code must perform same action as `process()` + * + * @param non-empty-string $name The variable name without $ + * @param AttributesProcessorGenerator $generator Code generator for the "configureBuilder" method + * @param ReflectionAttribute $attribute Attribute to use + * + * @return void + */ + public function generateCode(string $name, AttributesProcessorGenerator $generator, ReflectionAttribute $attribute): void; } diff --git a/src/Processor/Element/ExtractorAttributeProcessor.php b/src/Processor/Element/ExtractorAttributeProcessor.php index f5d5e03..9a25597 100644 --- a/src/Processor/Element/ExtractorAttributeProcessor.php +++ b/src/Processor/Element/ExtractorAttributeProcessor.php @@ -2,6 +2,7 @@ namespace Bdf\Form\Attribute\Processor\Element; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\PropertyAccess\ExtractorInterface; @@ -30,4 +31,15 @@ public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->extractor($attribute); } + + /** + * {@inheritdoc} + */ + public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + { + // @todo refactor + /** @var class-string $constraint */ + $constraint = $attribute->getName(); + $generator->line('$?->extractor(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + } } diff --git a/src/Processor/Element/FilterAttributeProcessor.php b/src/Processor/Element/FilterAttributeProcessor.php index 4457d77..033b10d 100644 --- a/src/Processor/Element/FilterAttributeProcessor.php +++ b/src/Processor/Element/FilterAttributeProcessor.php @@ -2,8 +2,11 @@ namespace Bdf\Form\Attribute\Processor\Element; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Filter\FilterInterface; +use Nette\PhpGenerator\Literal; /** * Add the filter by calling filter() @@ -30,4 +33,15 @@ public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->filter($attribute); } + + /** + * {@inheritdoc} + */ + public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + { + // @todo refactor + /** @var class-string $constraint */ + $constraint = $attribute->getName(); + $generator->line('$?->filter(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + } } diff --git a/src/Processor/Element/HydratorAttributeProcessor.php b/src/Processor/Element/HydratorAttributeProcessor.php index 4e719e9..f684f07 100644 --- a/src/Processor/Element/HydratorAttributeProcessor.php +++ b/src/Processor/Element/HydratorAttributeProcessor.php @@ -2,8 +2,11 @@ namespace Bdf\Form\Attribute\Processor\Element; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\PropertyAccess\HydratorInterface; +use Nette\PhpGenerator\Literal; /** * Define as hydrator by calling hydrator() @@ -30,4 +33,15 @@ public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->hydrator($attribute); } + + /** + * {@inheritdoc} + */ + public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + { + // @todo refactor + /** @var class-string $constraint */ + $constraint = $attribute->getName(); + $generator->line('$?->hydrator(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + } } diff --git a/src/Processor/Element/TransformerAttributeProcessor.php b/src/Processor/Element/TransformerAttributeProcessor.php index 96ddfe1..719e953 100644 --- a/src/Processor/Element/TransformerAttributeProcessor.php +++ b/src/Processor/Element/TransformerAttributeProcessor.php @@ -2,9 +2,12 @@ namespace Bdf\Form\Attribute\Processor\Element; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; +use Nette\PhpGenerator\Literal; /** * Add the transformer by calling transformer() @@ -31,4 +34,15 @@ public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->transformer($attribute); } + + /** + * {@inheritdoc} + */ + public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + { + // @todo refactor + /** @var class-string $constraint */ + $constraint = $attribute->getName(); + $generator->line('$?->transformer(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + } } diff --git a/src/Processor/GenerateConfiguratorStrategy.php b/src/Processor/GenerateConfiguratorStrategy.php new file mode 100644 index 0000000..d991ebf --- /dev/null +++ b/src/Processor/GenerateConfiguratorStrategy.php @@ -0,0 +1,181 @@ + + */ + 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 + */ + 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()); + } + + /** + * Register a new processor for element attributes + * + * @param ElementAttributeProcessorInterface $processor + * + * @return void + * + * @template T as object + */ + public function registerElementAttributeProcessor(ElementAttributeProcessorInterface $processor): void + { + $this->elementProcessors[] = $processor; + } + + /** + * {@inheritdoc} + */ + public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void + { + foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->generateCodeForFormBuilder($this->generator, $form); + } + + $this->generator->line(); + } + + /** + * {@inheritdoc} + */ + public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void + { + $this->generator->line('$builder->submit(?)', [$name]); + + foreach ($property->getAttributes(ButtonBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->generateCodeForButtonBuilder($this->generator, $form); + } + + $this->generator->line(";\n"); + } + + /** + * {@inheritdoc} + */ + public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void + { + $elementType = $this->generator->useAndSimplifyType($elementType); + $this->generator->line('$? = $builder->add(?, ?::class);', [$name, $name, new Literal($elementType)]); + + foreach ($property->getAttributes(ChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute->newInstance()->generateCodeForChildBuilder($name, $this->generator, $form); + } + + foreach ($this->elementProcessors as $configurator) { + foreach ($property->getAttributes($configurator->type(), ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + $configurator->generateCode($name, $this->generator, $attribute); + } + } + } + + $this->generator->line(); // Add empty line + } + + /** + * {@inheritdoc} + */ + public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface + { + $this->generator->line('return $this;'); + + $method = $this->generator + ->implements(PostConfigureInterface::class) + ->implementsMethod(PostConfigureInterface::class, 'postConfigure') + ; + + if (!empty($buttonProperties)) { + $method->addBody('$root = $form->root();'); + } + + $scopedProperties = []; + + foreach ($elementProperties as $name => $property) { + if ($property->isPublic()) { + $method->addBody('$form->? = $inner[?]->element();', [$name, $name]); + } else { + $scopedProperties[$property->getDeclaringClass()->getName()][$name] = ['$form->? = $inner[?]->element();', [$name, $name]]; + } + } + + foreach ($buttonProperties as $name => $property) { + if ($property->isPublic()) { + $method->addBody('$form->? = $root->button(?);', [$name, $name]); + } else { + $scopedProperties[$property->getDeclaringClass()->getName()][$name] = ['$form->? = $root->button(?);', [$name, $name]]; + } + } + + foreach ($scopedProperties as $className => $lines) { + $closure = new Closure(); + $closure->addUse('inner'); + $closure->addUse('form'); + + if (!empty($buttonProperties)) { + $closure->addUse('root'); + } + + array_map(fn ($line) => $closure->addBody(...$line), $lines); + + $method->addBody( + '(\Closure::bind(?, null, ?::class))();', + [ + new Literal($this->generator->printer()->printClosure($closure)), + new Literal($this->generator->useAndSimplifyType($className)), + ] + ); + } + + return null; + } + + /** + * Print the generated class code + * + * @return string + * + * @see AttributesProcessorGenerator::print() + */ + public function code(): string + { + return $this->generator->print(); + } +} diff --git a/src/Processor/ReflectionProcessor.php b/src/Processor/ReflectionProcessor.php index f30e9e8..d01f417 100644 --- a/src/Processor/ReflectionProcessor.php +++ b/src/Processor/ReflectionProcessor.php @@ -69,7 +69,7 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil } } - return new PostConfigureReflectionSetProperties($elementProperties, $buttonProperties); + return $this->strategy->onPostConfigure($elementProperties, $buttonProperties, $form); } /** diff --git a/src/Processor/ReflectionStrategyInterface.php b/src/Processor/ReflectionStrategyInterface.php index e128a86..719ae0b 100644 --- a/src/Processor/ReflectionStrategyInterface.php +++ b/src/Processor/ReflectionStrategyInterface.php @@ -58,4 +58,12 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att * @return void */ public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void; + + /** + * @param array $elementProperties + * @param array $buttonProperties + * @param AttributeForm $form + * @return PostConfigureInterface|null + */ + public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface; } diff --git a/tests/Aggregate/ArrayConstraintTest.php b/tests/Aggregate/ArrayConstraintTest.php index 527930b..4ef93d9 100644 --- a/tests/Aggregate/ArrayConstraintTest.php +++ b/tests/Aggregate/ArrayConstraintTest.php @@ -3,19 +3,23 @@ namespace Tests\Form\Attribute\Aggregate; use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilder; use Bdf\Form\Attribute\Aggregate\ArrayConstraint; use Bdf\Form\Attribute\AttributeForm; -use PHPUnit\Framework\TestCase; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Symfony\Component\Validator\Constraints\Unique; +use Tests\Form\Attribute\TestCase; class ArrayConstraintTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[ArrayConstraint(Unique::class, ['message' => 'Not unique'])] public ArrayElement $values; }; @@ -27,4 +31,51 @@ public function test() $form->submit(['values' => ['aaa', 'bbb']]); $this->assertTrue($form->valid()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[ArrayConstraint(Unique::class, ['message' => 'Not unique'])] + public ArrayElement $values; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Symfony\Component\Validator\Constraints\Unique; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $values = $builder->add('values', ArrayElement::class); + $values->arrayConstraint(Unique::class, ['message' => 'Not unique']); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->values = $inner['values']->element(); + } +} + +PHP + , $form); + } } diff --git a/tests/Aggregate/CountTest.php b/tests/Aggregate/CountTest.php index b67451d..378ad4c 100644 --- a/tests/Aggregate/CountTest.php +++ b/tests/Aggregate/CountTest.php @@ -3,18 +3,22 @@ namespace Tests\Form\Attribute\Aggregate; use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilder; use Bdf\Form\Attribute\Aggregate\Count; use Bdf\Form\Attribute\AttributeForm; -use PHPUnit\Framework\TestCase; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; +use Tests\Form\Attribute\TestCase; class CountTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Count(min: 3, max: 5)] public ArrayElement $values; }; @@ -30,4 +34,51 @@ public function test() $form->submit(['values' => ['aaa', 'bbb', 'ccc']]); $this->assertTrue($form->valid()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Count(min: 3, max: 5)] + public ArrayElement $values; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Symfony\Component\Validator\Constraints\Count; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $values = $builder->add('values', ArrayElement::class); + $values->arrayConstraint(new Count(min: 3, max: 5)); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->values = $inner['values']->element(); + } +} + +PHP + , $form); + } } diff --git a/tests/Aggregate/ElementTypeTest.php b/tests/Aggregate/ElementTypeTest.php index cd41b0c..87b87f9 100644 --- a/tests/Aggregate/ElementTypeTest.php +++ b/tests/Aggregate/ElementTypeTest.php @@ -3,23 +3,27 @@ namespace Tests\Form\Attribute\Aggregate; use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilder; use Bdf\Form\Attribute\Aggregate\ElementType; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Form\Generates; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class ElementTypeTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test_simple() + public function test_simple(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[ElementType(IntegerElement::class), Setter] public ArrayElement $values; }; @@ -31,11 +35,11 @@ public function test_simple() } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_with_configurator() + public function test_with_configurator(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[ElementType(IntegerElement::class, "configureField"), Setter] public ArrayElement $values; @@ -52,11 +56,11 @@ public function configureField(IntegerElementBuilder $builder): void } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_with_embedded() + public function test_with_embedded(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[ElementType(EmbeddedForm::class), Setter] public ArrayElement $values; }; @@ -66,6 +70,60 @@ public function test_with_embedded() $this->assertEquals(['values' => [new Struct('az', 'er'), new Struct('ty', 'ui')]], $form->value()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[ElementType(IntegerElement::class, "configureField"), Setter] + public ArrayElement $values; + + public function configureField(IntegerElementBuilder $builder): void + { + $builder->min(200); + } + }; + + $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\Leaf\IntegerElement; +use Bdf\Form\PropertyAccess\Setter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $values = $builder->add('values', ArrayElement::class); + $values->element(IntegerElement::class, [$form, 'configureField']); + $values->hydrator(new Setter()); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->values = $inner['values']->element(); + } +} + +PHP + , $form); + } } #[Generates(Struct::class)] diff --git a/tests/Button/GroupsTest.php b/tests/Button/GroupsTest.php index babe53a..f2cf2e7 100644 --- a/tests/Button/GroupsTest.php +++ b/tests/Button/GroupsTest.php @@ -2,16 +2,23 @@ namespace Tests\Form\Attribute\Button; +use Bdf\Form\Aggregate\FormBuilder; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Button\Groups; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Button\ButtonInterface; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class GroupsTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Groups('foo', 'bar')] public ButtonInterface $btn; }; @@ -22,4 +29,51 @@ public function test() $form->submit(['btn' => 'ok']); $this->assertEquals(['foo', 'bar'], $form->root()->constraintGroups()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Groups('foo', 'bar')] + public ButtonInterface $btn; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->submit('btn') + ->groups(['foo', 'bar']) + ; + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $root = $form->root(); + $form->btn = $root->button('btn'); + } +} + +PHP + , $form); + } } diff --git a/tests/Button/ValueTest.php b/tests/Button/ValueTest.php index bb81bcb..0d52422 100644 --- a/tests/Button/ValueTest.php +++ b/tests/Button/ValueTest.php @@ -2,19 +2,24 @@ namespace Tests\Form\Attribute\Button; +use Bdf\Form\Aggregate\FormBuilder; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Button\Groups; use Bdf\Form\Attribute\Button\Value; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Button\ButtonInterface; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class ValueTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Value('foo')] public ButtonInterface $button; }; @@ -22,4 +27,51 @@ public function test() $this->assertFalse($form->submit(['button' => 'ok'])->button->clicked()); $this->assertTrue($form->submit(['button' => 'foo'])->button->clicked()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Value('foo')] + public ButtonInterface $button; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->submit('button') + ->value('foo') + ; + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $root = $form->root(); + $form->button = $root->button('button'); + } +} + +PHP + , $form); + } } diff --git a/tests/Child/CallbackModelTransformerTest.php b/tests/Child/CallbackModelTransformerTest.php index 8122fc6..0469288 100644 --- a/tests/Child/CallbackModelTransformerTest.php +++ b/tests/Child/CallbackModelTransformerTest.php @@ -5,20 +5,21 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\CallbackModelTransformer; use Bdf\Form\Attribute\Form\Generates; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class CallbackModelTransformerTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new #[Generates(Struct::class)] class extends AttributeForm { + $form = new #[Generates(Struct::class)] class(null, $processor) extends AttributeForm { #[CallbackModelTransformer('aTransformer'), Getter, Setter] public StringElement $a; #[CallbackModelTransformer(toEntity: 'bToEntity', toInput: 'bToInput'), Getter, Setter] @@ -49,11 +50,11 @@ public function bToInput($value, IntegerElement $input) } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_with_only_one_transformation_method() + public function test_with_only_one_transformation_method(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[CallbackModelTransformer(toEntity: 't'), Getter, Setter] public IntegerElement $foo; #[CallbackModelTransformer(toInput: 't'), Getter, Setter] @@ -75,4 +76,116 @@ public function t($value, $input) $this->assertSame(5, $form->foo->value()); $this->assertSame(6, $form->bar->value()); } + + /** + * + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[CallbackModelTransformer(toEntity: 't'), Getter, Setter] + public IntegerElement $foo; + #[CallbackModelTransformer(toInput: 't'), Getter, Setter] + public IntegerElement $bar; + + public function t($value, $input) + { + return $value + 1; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\ElementInterface; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; +use Bdf\Form\Transformer\TransformerInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', IntegerElement::class); + $foo->modelTransformer(new class ($form) implements TransformerInterface { + public $form; + + public function __construct($form) + { + $this->form = $form; + } + + /** + * {@inheritdoc} + */ + function transformToHttp($value, ElementInterface $input) + { + return $value; + } + + /** + * {@inheritdoc} + */ + function transformFromHttp($value, ElementInterface $input) + { + return $this->form->t($value, $input); + } + }); + $foo->hydrator(new Setter()); + $foo->extractor(new Getter()); + + $bar = $builder->add('bar', IntegerElement::class); + $bar->modelTransformer(new class ($form) implements TransformerInterface { + public $form; + + public function __construct($form) + { + $this->form = $form; + } + + /** + * {@inheritdoc} + */ + function transformToHttp($value, ElementInterface $input) + { + return $this->form->t($value, $input); + } + + /** + * {@inheritdoc} + */ + function transformFromHttp($value, ElementInterface $input) + { + return $value; + } + }); + $bar->hydrator(new Setter()); + $bar->extractor(new Getter()); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Child/ConfigureTest.php b/tests/Child/ConfigureTest.php index 98f88d2..a557b54 100644 --- a/tests/Child/ConfigureTest.php +++ b/tests/Child/ConfigureTest.php @@ -4,19 +4,20 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\Configure; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\Leaf\StringElementBuilder; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class ConfigureTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Configure('configureFoo')] public StringElement $foo; @@ -36,4 +37,56 @@ public function configureFoo(ChildBuilderInterface $builder): void $form->submit(['foo' => 'abc']); $this->assertTrue($form->valid()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Configure('configureFoo')] + public StringElement $foo; + + /** + * @param ChildBuilderInterface|StringElementBuilder $builder + */ + public function configureFoo(ChildBuilderInterface $builder): void + { + $builder->length(['min' => 3]); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $form->configureFoo($foo); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Child/DefaultValueTest.php b/tests/Child/DefaultValueTest.php index 0b6e62e..a25ec79 100644 --- a/tests/Child/DefaultValueTest.php +++ b/tests/Child/DefaultValueTest.php @@ -2,19 +2,27 @@ namespace Tests\Form\Attribute\Child; +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilder; +use Bdf\Form\Attribute\Aggregate\ElementType; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\DefaultValue; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Leaf\IntegerElement; -use PHPUnit\Framework\TestCase; +use Bdf\Form\Leaf\IntegerElementBuilder; +use Bdf\Form\PropertyAccess\Setter; +use Tests\Form\Attribute\TestCase; class DefaultValueTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[DefaultValue(42)] public IntegerElement $v; }; @@ -22,4 +30,50 @@ public function test() $form->submit([]); $this->assertSame(42, $form->v->value()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[DefaultValue(42)] + public IntegerElement $v; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\IntegerElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $v = $builder->add('v', IntegerElement::class); + $v->default(42); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->v = $inner['v']->element(); + } +} + +PHP + , $form); + } } diff --git a/tests/Child/DependenciesTest.php b/tests/Child/DependenciesTest.php index a988d0a..e2b7982 100644 --- a/tests/Child/DependenciesTest.php +++ b/tests/Child/DependenciesTest.php @@ -2,17 +2,29 @@ namespace Tests\Form\Attribute\Child; +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilder; +use Bdf\Form\Attribute\Aggregate\ElementType; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\Dependencies; use Bdf\Form\Attribute\Element\CallbackTransformer; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Bdf\Form\PropertyAccess\Setter; +use Tests\Form\Attribute\TestCase; class DependenciesTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { public StringElement $foo; #[Dependencies('foo', 'bar'), CallbackTransformer(fromHttp: 'bazTransformer')] public StringElement $baz; @@ -27,4 +39,58 @@ public function bazTransformer($value) $form->submit(['foo' => 'a', 'bar' => 'b', 'baz' => 'c']); $this->assertSame('acb', $form->baz->value()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + public StringElement $foo; + #[Dependencies('foo', 'bar')] + public StringElement $baz; + public StringElement $bar; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + + $baz = $builder->add('baz', StringElement::class); + $baz->depends('foo', 'bar'); + + $bar = $builder->add('bar', StringElement::class); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->baz = $inner['baz']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form); + } } diff --git a/tests/Child/GetSetTest.php b/tests/Child/GetSetTest.php index 39a3e0d..bf483e7 100644 --- a/tests/Child/GetSetTest.php +++ b/tests/Child/GetSetTest.php @@ -2,16 +2,28 @@ namespace Tests\Form\Attribute\Child; +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilder; +use Bdf\Form\Attribute\Aggregate\ElementType; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\GetSet; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Bdf\Form\PropertyAccess\Setter; +use Tests\Form\Attribute\TestCase; class GetSetTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[GetSet] public StringElement $a; @@ -26,4 +38,52 @@ public function test() $this->assertSame('aaa', $form->a->value()); $this->assertSame('ccc', $form->b->value()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[GetSet('c')] + public StringElement $b; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $b = $builder->add('b', StringElement::class); + $b->hydrator(new Setter('c'))->extractor(new Getter('c')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->b = $inner['b']->element(); + } +} + +PHP + , $form); + } } diff --git a/tests/Child/ModelTransformerTest.php b/tests/Child/ModelTransformerTest.php index 5c3ac21..9e99171 100644 --- a/tests/Child/ModelTransformerTest.php +++ b/tests/Child/ModelTransformerTest.php @@ -6,21 +6,22 @@ use Bdf\Form\Attribute\Child\GetSet; use Bdf\Form\Attribute\Child\ModelTransformer; use Bdf\Form\Attribute\Form\Generates; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\ElementInterface; use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; use Bdf\Form\Transformer\TransformerInterface; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; // @todo Raw attribute for number elements class ModelTransformerTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { $form = new #[Generates(Struct::class)] class extends AttributeForm { #[ModelTransformer(ATransformer::class), Getter, Setter] @@ -45,6 +46,77 @@ public function test() $form->import(new Struct(c: 'foo_abc')); $this->assertEquals('abc', $form->c->value()); } + + public function test_code_generator() + { + $form = new #[Generates(Struct::class)] class extends AttributeForm { + #[ModelTransformer(ATransformer::class), Getter, Setter] + public StringElement $a; + #[ModelTransformer(BTransformer::class), Getter, Setter] + public IntegerElement $b; + + #[ModelTransformer(TransformerWithArguments::class, ['foo_']), GetSet] + public StringElement $c; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; +use Tests\Form\Attribute\Child\ATransformer; +use Tests\Form\Attribute\Child\BTransformer; +use Tests\Form\Attribute\Child\Struct; +use Tests\Form\Attribute\Child\TransformerWithArguments; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->generates(Struct::class); + + $a = $builder->add('a', StringElement::class); + $a->modelTransformer(new ATransformer()); + $a->hydrator(new Setter()); + $a->extractor(new Getter()); + + $b = $builder->add('b', IntegerElement::class); + $b->modelTransformer(new BTransformer()); + $b->hydrator(new Setter()); + $b->extractor(new Getter()); + + $c = $builder->add('c', StringElement::class); + $c->modelTransformer(new TransformerWithArguments('foo_')); + $c->hydrator(new Setter(null))->extractor(new Getter(null)); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->a = $inner['a']->element(); + $form->b = $inner['b']->element(); + $form->c = $inner['c']->element(); + } +} + +PHP + , $form +); + } } class Struct diff --git a/tests/Constraint/CustomConstraintTest.php b/tests/Constraint/CustomConstraintTest.php index 7ae9b71..19058c2 100644 --- a/tests/Constraint/CustomConstraintTest.php +++ b/tests/Constraint/CustomConstraintTest.php @@ -4,17 +4,24 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Constraint\CustomConstraint; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class CustomConstraintTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[CustomConstraint('validateFoo', message: 'Foo length must be a multiple of 2')] public StringElement $foo; + #[CustomConstraint('validateFoo')] + public StringElement $bar; + public function validateFoo($value): bool { return strlen($value) % 2 === 0; @@ -30,5 +37,65 @@ public function validateFoo($value): bool $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 { + #[CustomConstraint('validateFoo', message: 'Foo length must be a multiple of 2')] + public StringElement $foo; + + public function validateFoo($value): bool + { + return strlen($value) % 2 === 0; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Constraint\Closure as ClosureConstraint; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->satisfy(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo length must be a multiple of 2'])); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); } } diff --git a/tests/Constraint/SatisfyTest.php b/tests/Constraint/SatisfyTest.php index 80192ac..0c15fda 100644 --- a/tests/Constraint/SatisfyTest.php +++ b/tests/Constraint/SatisfyTest.php @@ -4,15 +4,19 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Constraint\Satisfy; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\Length; class SatisfyTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Satisfy(Length::class, ['min' => 3])] public StringElement $foo; }; @@ -24,4 +28,49 @@ public function test() $form->submit(['foo' => 'abc']); $this->assertTrue($form->valid()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Satisfy(Length::class, ['min' => 3])] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Symfony\Component\Validator\Constraints\Length; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->satisfy(Length::class, ['min' => 3]); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Element/CallbackTransformerTest.php b/tests/Element/CallbackTransformerTest.php index 4ec3ff4..24021e0 100644 --- a/tests/Element/CallbackTransformerTest.php +++ b/tests/Element/CallbackTransformerTest.php @@ -5,18 +5,19 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\GetSet; use Bdf\Form\Attribute\Element\CallbackTransformer; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class CallbackTransformerTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[CallbackTransformer('fooTransformer')] public StringElement $foo; @@ -51,11 +52,11 @@ public function outTransformer($value, StringElement $input) } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_with_only_one_transformation_method() + public function test_with_only_one_transformation_method(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[CallbackTransformer(fromHttp: 't'), GetSet] public IntegerElement $foo; #[CallbackTransformer(toHttp: 't'), GetSet] @@ -78,4 +79,94 @@ public function t($value, $input) $this->assertEquals(5, $view['foo']->value()); $this->assertEquals(6, $view['bar']->value()); } + + public function test_code_generator() + { + $form = new class() extends AttributeForm { + #[CallbackTransformer('fooTransformer')] + public StringElement $foo; + + #[CallbackTransformer(fromHttp: 'inTransformer', toHttp: 'outTransformer')] + public StringElement $bar; + + public function fooTransformer($value, StringElement $input, bool $toPhp) + { + return json_encode([$value, $toPhp]); + } + + public function inTransformer($value, StringElement $input) + { + return json_encode(['in', $value]); + } + + public function outTransformer($value, StringElement $input) + { + return json_encode(['out', $value]); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\ElementInterface; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\Transformer\TransformerInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->transformer([$form, 'fooTransformer']); + + $bar = $builder->add('bar', StringElement::class); + $bar->transformer(new class ($form) implements TransformerInterface { + public $form; + + public function __construct($form) + { + $this->form = $form; + } + + /** + * {@inheritdoc} + */ + function transformToHttp($value, ElementInterface $input) + { + return $this->form->outTransformer($value, $input); + } + + /** + * {@inheritdoc} + */ + function transformFromHttp($value, ElementInterface $input) + { + return $this->form->inTransformer($value, $input); + } + }); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form); + } } diff --git a/tests/Element/ChoicesTest.php b/tests/Element/ChoicesTest.php index b206c6e..d455d09 100644 --- a/tests/Element/ChoicesTest.php +++ b/tests/Element/ChoicesTest.php @@ -5,18 +5,19 @@ use Bdf\Form\Aggregate\ArrayElement; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Element\Choices; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Choice\ArrayChoice; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class ChoicesTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Choices(choices: ['foo', 'bar'], message: 'my error')] public StringElement $foo; @@ -45,4 +46,69 @@ public function generateChoices() $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 { + #[Choices(choices: ['foo', 'bar'], message: 'my error')] + public StringElement $foo; + + #[Choices(choices: ['foo', 'bar', 'baz'], message: 'my error', options: ['min' => 2])] + public ArrayElement $bar; + + #[Choices('generateChoices')] + public StringElement $baz; + + public function generateChoices() + { + return ['aaa', 'bbb', 'ccc']; + } + }; + + $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\Choice\LazzyChoice; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->choices(['foo', 'bar'], ['multipleMessage' => 'my error', 'message' => 'my error']); + + $bar = $builder->add('bar', ArrayElement::class); + $bar->choices(['foo', 'bar', 'baz'], ['min' => 2, 'multipleMessage' => 'my error', 'message' => 'my error']); + + $baz = $builder->add('baz', StringElement::class); + $baz->choices(new LazzyChoice([$form, 'generateChoices']), []); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + $form->baz = $inner['baz']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Element/IgnoreTransformerExceptionTest.php b/tests/Element/IgnoreTransformerExceptionTest.php index 35f72ac..2f2ab6d 100644 --- a/tests/Element/IgnoreTransformerExceptionTest.php +++ b/tests/Element/IgnoreTransformerExceptionTest.php @@ -5,17 +5,18 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Element\CallbackTransformer; use Bdf\Form\Attribute\Element\IgnoreTransformerException; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class IgnoreTransformerExceptionTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[IgnoreTransformerException, CallbackTransformer('transform')] public StringElement $foo; @@ -33,4 +34,62 @@ public function transform() $this->assertFalse($form->valid()); $this->assertEquals(['bar' => 'My error'], $form->error()->toArray()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[IgnoreTransformerException, CallbackTransformer('transform')] + public StringElement $foo; + + #[IgnoreTransformerException(false), CallbackTransformer('transform')] + public StringElement $bar; + + public function transform() + { + throw new \Exception('My error'); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->ignoreTransformerException(true); + $foo->transformer([$form, 'transform']); + + $bar = $builder->add('bar', StringElement::class); + $bar->ignoreTransformerException(false); + $bar->transformer([$form, 'transform']); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Element/TransformerErrorTest.php b/tests/Element/TransformerErrorTest.php index 1daf853..31e8511 100644 --- a/tests/Element/TransformerErrorTest.php +++ b/tests/Element/TransformerErrorTest.php @@ -5,19 +5,20 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Element\CallbackTransformer; use Bdf\Form\Attribute\Element\TransformerError; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\Validator\TransformerExceptionConstraint; use http\Message; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class TransformerErrorTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test() + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[CallbackTransformer('transformer'), TransformerError('my message')] public StringElement $foo; #[CallbackTransformer('transformer'), TransformerError(message: 'bar', code: 'BAR_ERROR')] @@ -36,11 +37,11 @@ public function transformer() } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_with_callback() + public function test_with_callback(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[CallbackTransformer('transformer'), TransformerError(validationCallback: 'handleError')] public StringElement $foo; @@ -69,4 +70,66 @@ public function handleError($value, TransformerExceptionConstraint $constraint) $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 { + #[CallbackTransformer('transformer'), TransformerError('my message')] + public StringElement $foo; + #[CallbackTransformer('transformer'), TransformerError(message: 'bar', code: 'BAR_ERROR')] + public StringElement $bar; + + public function transformer() + { + throw new \Exception('My error'); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->transformer([$form, 'transformer']); + $foo + ->transformerErrorMessage('my message') + ; + + $bar = $builder->add('bar', StringElement::class); + $bar->transformer([$form, 'transformer']); + $bar + ->transformerErrorMessage('bar') + ->transformerErrorCode('BAR_ERROR') + ; + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Element/TransformerTest.php b/tests/Element/TransformerTest.php index 3e87836..6da284c 100644 --- a/tests/Element/TransformerTest.php +++ b/tests/Element/TransformerTest.php @@ -4,16 +4,20 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Element\Transformer; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\ElementInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\Transformer\TransformerInterface; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class TransformerTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Transformer(ATransformer::class, ['A'])] public StringElement $foo; }; @@ -24,6 +28,51 @@ public function test() $view = $form->view(); $this->assertEquals('A_A', $view['foo']->value()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Transformer(ATransformer::class, ['A'])] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Tests\Form\Attribute\Element\ATransformer; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->transformer(new ATransformer('A')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } } class ATransformer implements TransformerInterface diff --git a/tests/Form/CallbackGeneratorTest.php b/tests/Form/CallbackGeneratorTest.php index df16ba7..092761d 100644 --- a/tests/Form/CallbackGeneratorTest.php +++ b/tests/Form/CallbackGeneratorTest.php @@ -5,13 +5,20 @@ use Bdf\Form\Aggregate\FormInterface; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Form\CallbackGenerator; +use Bdf\Form\Attribute\Form\Generates; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class CallbackGeneratorTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { $form = new #[CallbackGenerator('generate')] class extends AttributeForm { #[Setter] @@ -26,4 +33,49 @@ public function generate(FormInterface $form) $form->submit(['foo' => 'b']); $this->assertEquals((object) ['foo' => 'b', 'bar' => 'a'], $form->value()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new #[CallbackGenerator('generate')] class extends AttributeForm { + public function generate(FormInterface $form) + { + return (object) ['foo' => null, 'bar' => 'a']; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->generates([$this, 'generate']); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + } +} + +PHP + , $form); + } } diff --git a/tests/Form/GeneratesTest.php b/tests/Form/GeneratesTest.php index 79e19fb..beff772 100644 --- a/tests/Form/GeneratesTest.php +++ b/tests/Form/GeneratesTest.php @@ -3,21 +3,28 @@ namespace Tests\Form\Attribute\Form; use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\Value\MyEntity; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Form\Generates; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Custom\CustomForm; use Bdf\Form\Leaf\FloatElement; use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\NotEqualTo; use Symfony\Component\Validator\Constraints\Positive; class GeneratesTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { $form = new #[Generates(Person::class)] class extends AttributeForm { #[Setter] @@ -36,6 +43,48 @@ public function test() $expected->age = 35; $this->assertEquals($expected, $form->value()); } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new #[Generates(Person::class)] class extends AttributeForm { + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Tests\Form\Attribute\Form\Person; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->generates(Person::class); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + } +} + +PHP + , $form); + } } class Person diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index b854a24..c8c8d99 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -4,6 +4,7 @@ use Attribute; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Button\ButtonInterface; use Bdf\Form\Button\SubmitButton; use Bdf\Form\Child\ChildInterface; @@ -14,7 +15,7 @@ use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; use Bdf\Form\Transformer\TransformerInterface; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\GreaterThan; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; @@ -22,11 +23,11 @@ class FunctionalTest extends TestCase { /** - * + * @dataProvider provideAttributesProcessor */ - public function test_simple() + public function test_simple(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[NotBlank, Length(min: 3), Getter, Setter] public StringElement $firstName; @@ -64,11 +65,11 @@ public function test_simple() } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_setter_with_name() + public function test_setter_with_name(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Setter('bar')] public StringElement $foo; }; @@ -78,11 +79,11 @@ public function test_setter_with_name() } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_getter_with_name() + public function test_getter_with_name(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Getter('bar')] public StringElement $foo; }; @@ -92,11 +93,11 @@ public function test_getter_with_name() } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_inheritance() + public function test_inheritance(AttributesProcessorInterface $processor) { - $form = new ChildForm(); + $form = new ChildForm(null, $processor); $this->assertInstanceOf(StringElement::class, $form['foo']->element()); $this->assertInstanceOf(IntegerElement::class, $form['bar']->element()); @@ -115,9 +116,74 @@ public function test_inheritance() /** * */ - public function test_buttons() + public function test_inheritance_code_generator() + { + $form = new ChildForm(); + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; +use Symfony\Component\Validator\Constraints\GreaterThan; +use Symfony\Component\Validator\Constraints\NotBlank; +use Tests\Form\Attribute\BaseForm; +use Tests\Form\Attribute\ChildForm; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $bar = $builder->add('bar', IntegerElement::class); + $bar->satisfy(new NotBlank()); + $bar->satisfy(new GreaterThan(5)); + $bar->hydrator(new Setter()); + $bar->extractor(new Getter()); + + + $foo = $builder->add('foo', StringElement::class); + $foo->satisfy(new NotBlank()); + $foo->hydrator(new Setter()); + $foo->extractor(new Getter()); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void { - $form = new class extends AttributeForm { + (\Closure::bind(function () use ($inner, $form) { + $form->bar = $inner['bar']->element(); + }, null, ChildForm::class))(); + (\Closure::bind(function () use ($inner, $form) { + $form->foo = $inner['foo']->element(); + }, null, BaseForm::class))(); + } +} + +PHP + , $form +); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_buttons(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { public ButtonInterface $foo; public ButtonInterface $bar; }; @@ -142,11 +208,11 @@ public function test_buttons() } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_filter() + public function test_filter(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[FilterVar(FILTER_SANITIZE_FULL_SPECIAL_CHARS), Setter] public StringElement $foo; }; @@ -156,11 +222,11 @@ public function test_filter() } /** - * + * @dataProvider provideAttributesProcessor */ - public function test_transformer() + public function test_transformer(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[MyTransformer, Setter] public StringElement $foo; }; diff --git a/tests/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php b/tests/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php new file mode 100644 index 0000000..6be5591 --- /dev/null +++ b/tests/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php @@ -0,0 +1,121 @@ +expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The class name must have a namespace'); + + new AttributesProcessorGenerator('MyClass'); + } + + /** + * @return void + */ + public function test_empty() + { + $generator = new AttributesProcessorGenerator('Generated\Processor'); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class Processor implements AttributesProcessorInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + } +} + +PHP + , $generator->print() +); + } + + /** + * @return void + */ + public function test_line() + { + $generator = new AttributesProcessorGenerator('Generated\Processor'); + + $generator->line('$?->?(...?);', ['foo', 'bar', [1, 2, 3]]); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class Processor implements AttributesProcessorInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo->bar(1, 2, 3); + } +} + +PHP + , $generator->print() +); + } + + /** + * @return void + */ + public function test_new() + { + $generator = new AttributesProcessorGenerator('Generated\Processor'); + $expression = $generator->new(Count::class, ['min' => 3, 'max' => 6]); + + $this->assertEquals('new Count(min: 3, max: 6)', (string) $expression); + + $generator->line('$builder->satisfy(?);', [$expression]); + + $this->assertEquals(<<<'PHP' +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; + +class Processor implements AttributesProcessorInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->satisfy(new Count(min: 3, max: 6)); + } +} + +PHP + , $generator->print() +); + } +} diff --git a/tests/Processor/CodeGenerator/ClassGeneratorTest.php b/tests/Processor/CodeGenerator/ClassGeneratorTest.php new file mode 100644 index 0000000..02d8d48 --- /dev/null +++ b/tests/Processor/CodeGenerator/ClassGeneratorTest.php @@ -0,0 +1,139 @@ +implements(PostConfigureInterface::class); + + $this->assertEquals(<<<'PHP' +class Foo implements PostConfigureInterface +{ +} + +PHP + , $generator->generateClass() +); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + + +PHP + , $generator->printer()->printNamespace($generator->namespace()) +); + } + + /** + * @return void + */ + public function test_implementsMethod() + { + $generator = new ClassGenerator(new PhpNamespace('Generated'), new ClassType('Foo')); + $method = $generator->implementsMethod(PostConfigureInterface::class, 'postConfigure'); + + $this->assertEquals('postConfigure', $method->getName()); + + $this->assertEquals(<<<'PHP' +class Foo +{ + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + } +} + +PHP + , $generator->generateClass() +); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; + + +PHP + , $generator->printer()->printNamespace($generator->namespace()) +); + } + + /** + * @return void + */ + public function test_useAndSimplifyType() + { + $generator = new ClassGenerator(new PhpNamespace('Generated'), new ClassType('Foo')); + + $this->assertSame('StringElement', $generator->useAndSimplifyType(StringElement::class)); + $this->assertSame('WithAlias', $generator->useAndSimplifyType(IntegerElement::class, 'WithAlias')); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Leaf\IntegerElement as WithAlias; +use Bdf\Form\Leaf\StringElement; + + +PHP + , $generator->printer()->printNamespace($generator->namespace()) + ); + } + + /** + * @return void + */ + public function test_use() + { + $generator = new ClassGenerator(new PhpNamespace('Generated'), new ClassType('Foo')); + + $generator->use(StringElement::class)->use(IntegerElement::class, 'WithAlias'); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Leaf\IntegerElement as WithAlias; +use Bdf\Form\Leaf\StringElement; + + +PHP + , $generator->printer()->printNamespace($generator->namespace()) + ); + } + + /** + * @return void + */ + public function test_getters() + { + $namespace = new PhpNamespace('Generated'); + $class = new ClassType('Foo'); + $printer = new PsrPrinter(); + + $generator = new ClassGenerator($namespace, $class, $printer); + + $this->assertSame($namespace, $generator->namespace()); + $this->assertSame($class, $generator->class()); + $this->assertSame($printer, $generator->printer()); + } +} diff --git a/tests/Processor/CompileAttributesProcessorTest.php b/tests/Processor/CompileAttributesProcessorTest.php new file mode 100644 index 0000000..6898ac2 --- /dev/null +++ b/tests/Processor/CompileAttributesProcessorTest.php @@ -0,0 +1,259 @@ + 'Generated\\' . get_class($form) . 'Configurator', + fn (string $className) => '/tmp' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php' + ); + + $form = new MyForm(); + + $postConfigure = $processor->configureBuilder($form, new FormBuilder()); + + $this->assertFileExists('/tmp/Generated/Tests/Form/Attribute/Processor/MyFormConfigurator.php'); + $this->assertStringEqualsFile( + '/tmp/Generated/Tests/Form/Attribute/Processor/MyFormConfigurator.php', +<<<'PHP' +generates(Person::class); + + $firstName = $builder->add('firstName', StringElement::class); + $firstName->satisfy(new ClosureConstraint([$form, 'validateName'])); + $firstName->hydrator(new Setter(null))->extractor(new Getter(null)); + $firstName->satisfy(new NotBlank()); + + $lastName = $builder->add('lastName', StringElement::class); + $lastName->satisfy(new ClosureConstraint([$form, 'validateName'])); + $lastName->hydrator(new Setter(null))->extractor(new Getter(null)); + + $age = $builder->add('age', IntegerElement::class); + $age->hydrator(new Setter(null))->extractor(new Getter(null)); + $age->satisfy(new Positive()); + $age->satisfy(new LessThan(150)); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + (\Closure::bind(function () use ($inner, $form) { + $form->firstName = $inner['firstName']->element(); + $form->lastName = $inner['lastName']->element(); + $form->age = $inner['age']->element(); + }, null, MyForm::class))(); + } +} + +PHP + ); + + $this->assertTrue(class_exists('Generated\Tests\Form\Attribute\Processor\MyFormConfigurator', false)); + $this->assertInstanceOf('Generated\Tests\Form\Attribute\Processor\MyFormConfigurator', $postConfigure); + } + + public function test_file_already_exists_should_only_be_included() + { + $file = '/tmp/generated_configurator.php'; + + file_put_contents( + $file, +$code = <<<'PHP' + 'Generated\\Configurator', + fn (string $className) => $file + ); + + $form = new MyForm(); + + $postConfigure = $processor->configureBuilder($form, new FormBuilder()); + + $this->assertStringEqualsFile($file, $code); + + $this->assertTrue(class_exists('Generated\Configurator', false)); + $this->assertInstanceOf('Generated\Configurator', $postConfigure); + } + + public function test_file_already_exists_but_with_invalid_class_should_throw_error() + { + $file = '/tmp/invalid_class_configurator.php'; + + file_put_contents( + $file, +$code = <<<'PHP' + 'Generated\\InvalidConfigurator', + fn (string $className) => $file + ); + + $form = new MyForm(); + + try { + $processor->configureBuilder($form, new FormBuilder()); + $this->fail('expect LogicException'); + } catch (\LogicException $e) { + $this->assertEquals('Invalid generated class "Generated\InvalidConfigurator" in file "/tmp/invalid_class_configurator.php"', $e->getMessage()); + } + + $this->assertStringEqualsFile($file, $code); + } + + public function test_file_already_exists_but_without_class_on_file() + { + $file = '/tmp/invalid_class_configurator.php'; + + file_put_contents( + $file, +$code = <<<'PHP' + 'Generated\\NotAClass', + fn (string $className) => $file + ); + + $form = new MyForm(); + + try { + $processor->configureBuilder($form, new FormBuilder()); + $this->fail('expect LogicException'); + } catch (\LogicException $e) { + $this->assertEquals('Invalid generated class "Generated\NotAClass" in file "/tmp/invalid_class_configurator.php"', $e->getMessage()); + } + + $this->assertStringEqualsFile($file, $code); + } +} + +#[Generates(Person::class)] +class MyForm extends AttributeForm +{ + #[NotBlank, CustomConstraint('validateName'), GetSet] + private StringElement $firstName; + + #[CustomConstraint('validateName'), GetSet] + private StringElement $lastName; + + #[Positive, LessThan(150), GetSet] + private IntegerElement $age; + + public function validateName(?string $value, StringElement $input): bool + { + if (!$value) { + return true; + } + + return preg_match('#[a-z][a-z -]*#i', $value); + } +} + +class Person +{ + public function __construct( + public string $firstName, + public ?string $lastName = null, + public ?int $age = null, + ) { + } +} diff --git a/tests/Processor/ConfigureFormBuilderStrategyTest.php b/tests/Processor/ConfigureFormBuilderStrategyTest.php index 49ce03b..178c72e 100644 --- a/tests/Processor/ConfigureFormBuilderStrategyTest.php +++ b/tests/Processor/ConfigureFormBuilderStrategyTest.php @@ -3,12 +3,14 @@ namespace Tests\Form\Attribute\Processor; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\ConfigureFormBuilderStrategy; use Bdf\Form\Attribute\Processor\Element\ElementAttributeProcessorInterface; +use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class ConfigureFormBuilderStrategyTest extends TestCase { @@ -22,6 +24,11 @@ public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->default('Foo'); } + + public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + { + + } }); $form = new class(null, new ReflectionProcessor($strategy)) extends AttributeForm { diff --git a/tests/Processor/Element/ConstraintAttributeProcessorTest.php b/tests/Processor/Element/ConstraintAttributeProcessorTest.php index 39df5fe..0fa4812 100644 --- a/tests/Processor/Element/ConstraintAttributeProcessorTest.php +++ b/tests/Processor/Element/ConstraintAttributeProcessorTest.php @@ -3,16 +3,20 @@ namespace Tests\Form\Attribute\Processor\Element; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotEqualTo; class ConstraintAttributeProcessorTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Length(min: 5), NotEqualTo('azerty')] public StringElement $foo; }; @@ -30,4 +34,51 @@ public function test() $form->submit(['foo' => 'aqwzsx']); $this->assertTrue($form->valid()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Length(min: 5), NotEqualTo('azerty')] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotEqualTo; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->satisfy(new Length(min: 5)); + $foo->satisfy(new NotEqualTo('azerty')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Processor/Element/ExtractorAttributeProcessorTest.php b/tests/Processor/Element/ExtractorAttributeProcessorTest.php index dcec853..4bf2dd3 100644 --- a/tests/Processor/Element/ExtractorAttributeProcessorTest.php +++ b/tests/Processor/Element/ExtractorAttributeProcessorTest.php @@ -3,15 +3,19 @@ namespace Tests\Form\Attribute\Processor\Element; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class ExtractorAttributeProcessorTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Getter('bar')] public StringElement $foo; }; @@ -19,4 +23,49 @@ public function test() $form->import(['bar' => 'azerty']); $this->assertSame('azerty', $form->foo->value()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Getter('bar')] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Getter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->extractor(new Getter('bar')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } } diff --git a/tests/Processor/Element/FilterAttributeProcessorTest.php b/tests/Processor/Element/FilterAttributeProcessorTest.php index e53078b..dfae091 100644 --- a/tests/Processor/Element/FilterAttributeProcessorTest.php +++ b/tests/Processor/Element/FilterAttributeProcessorTest.php @@ -4,16 +4,21 @@ use Attribute; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Child\ChildInterface; use Bdf\Form\Filter\FilterInterface; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Bdf\Form\PropertyAccess\Getter; +use Tests\Form\Attribute\TestCase; class FilterAttributeProcessorTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[AFilter, BFilter] public StringElement $foo; }; @@ -21,6 +26,53 @@ public function test() $form->submit(['foo' => 'bar']); $this->assertEquals('barAB', $form->foo->value()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[AFilter, BFilter] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Tests\Form\Attribute\Processor\Element\AFilter; +use Tests\Form\Attribute\Processor\Element\BFilter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->filter(new AFilter()); + $foo->filter(new BFilter()); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form + ); + } } #[Attribute(Attribute::TARGET_PROPERTY)] diff --git a/tests/Processor/Element/HydratorAttributeProcessorTest.php b/tests/Processor/Element/HydratorAttributeProcessorTest.php index 379bd04..96b09f4 100644 --- a/tests/Processor/Element/HydratorAttributeProcessorTest.php +++ b/tests/Processor/Element/HydratorAttributeProcessorTest.php @@ -3,15 +3,19 @@ namespace Tests\Form\Attribute\Processor\Element; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class HydratorAttributeProcessorTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[Setter('bar')] public StringElement $foo; }; @@ -19,4 +23,48 @@ public function test() $form->submit(['foo' => 'azerty']); $this->assertSame(['bar' => 'azerty'], $form->value()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Setter('bar')] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Setter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->hydrator(new Setter('bar')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form); + } } diff --git a/tests/Processor/Element/TransformerAttributeProcessorTest.php b/tests/Processor/Element/TransformerAttributeProcessorTest.php index e29c1ae..7ebdb57 100644 --- a/tests/Processor/Element/TransformerAttributeProcessorTest.php +++ b/tests/Processor/Element/TransformerAttributeProcessorTest.php @@ -3,17 +3,22 @@ namespace Tests\Form\Attribute\Processor\Element; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Attribute\Processor\Element\TransformerAttributeProcessor; use Bdf\Form\ElementInterface; use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Setter; use Bdf\Form\Transformer\TransformerInterface; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class TransformerAttributeProcessorTest extends TestCase { - public function test() + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) { - $form = new class extends AttributeForm { + $form = new class(null, $processor) extends AttributeForm { #[ATransformer, BTransformer] public StringElement $foo; }; @@ -21,6 +26,52 @@ public function test() $form->submit(['foo' => 'azerty']); $this->assertEquals('azertyBA', $form->foo->value()); } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[ATransformer, BTransformer] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Tests\Form\Attribute\Processor\Element\ATransformer; +use Tests\Form\Attribute\Processor\Element\BTransformer; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->transformer(new ATransformer()); + $foo->transformer(new BTransformer()); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form); + } } #[\Attribute(\Attribute::TARGET_PROPERTY)] diff --git a/tests/Processor/GenerateConfiguratorStrategyTest.php b/tests/Processor/GenerateConfiguratorStrategyTest.php new file mode 100644 index 0000000..2b6f92d --- /dev/null +++ b/tests/Processor/GenerateConfiguratorStrategyTest.php @@ -0,0 +1,261 @@ +configureBuilder($form, new FormBuilder()); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Aggregate\Value\MyEntity; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->generates(MyEntity::class); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + } +} + +PHP + , $generator->code()); + } + + /** + * @return void + */ + public function test_button_properties() + { + $generator = new GenerateConfiguratorStrategy('Generated\GeneratedConfigurator'); + $form = new class extends AttributeForm { + public ButtonInterface $foo; + #[Value('bar'), Groups('aaa', 'bbb')] + public ButtonInterface $bar; + }; + + (new ReflectionProcessor($generator))->configureBuilder($form, new FormBuilder()); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->submit('foo') + ; + + $builder->submit('bar') + ->value('bar') + ->groups(['aaa', 'bbb']) + ; + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $root = $form->root(); + $form->foo = $root->button('foo'); + $form->bar = $root->button('bar'); + } +} + +PHP + , $generator->code()); + } + + /** + * @return void + */ + public function test_element_properties() + { + $generator = new GenerateConfiguratorStrategy('Generated\GeneratedConfigurator'); + $form = new class extends AttributeForm { + public StringElement $foo; + #[NotBlank, Positive] + public IntegerElement $bar; + }; + + (new ReflectionProcessor($generator))->configureBuilder($form, new FormBuilder()); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\Leaf\StringElement; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Positive; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + + $bar = $builder->add('bar', IntegerElement::class); + $bar->satisfy(new NotBlank()); + $bar->satisfy(new Positive()); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $generator->code()); + } + + /** + * @return void + */ + public function test_generate_post_configure_method_with_private_visiblity() + { + $generator = new GenerateConfiguratorStrategy('Generated\GeneratedConfigurator'); + + (new ReflectionProcessor($generator))->configureBuilder(new CForm(), new FormBuilder()); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Tests\Form\Attribute\Processor\AForm; +use Tests\Form\Attribute\Processor\BForm; +use Tests\Form\Attribute\Processor\CForm; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $d = $builder->add('d', StringElement::class); + + + $builder->submit('b') + ; + + $c = $builder->add('c', StringElement::class); + + + $a = $builder->add('a', StringElement::class); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $root = $form->root(); + (\Closure::bind(function () use ($inner, $form, $root) { + $form->d = $inner['d']->element(); + }, null, CForm::class))(); + (\Closure::bind(function () use ($inner, $form, $root) { + $form->c = $inner['c']->element(); + $form->b = $root->button('b'); + }, null, BForm::class))(); + (\Closure::bind(function () use ($inner, $form, $root) { + $form->a = $inner['a']->element(); + }, null, AForm::class))(); + } +} + +PHP + , $generator->code()); + } +} + +class AForm extends AttributeForm +{ + private StringElement $a; +} + +class BForm extends AForm +{ + private ButtonInterface $b; + private StringElement $c; +} + +class CForm extends BForm +{ + private StringElement $d; +} diff --git a/tests/Processor/ReflectionProcessorTest.php b/tests/Processor/ReflectionProcessorTest.php index b9a59fe..ec75444 100644 --- a/tests/Processor/ReflectionProcessorTest.php +++ b/tests/Processor/ReflectionProcessorTest.php @@ -8,7 +8,7 @@ use Bdf\Form\Attribute\Processor\ReflectionStrategyInterface; use Bdf\Form\Button\ButtonInterface; use Bdf\Form\Leaf\StringElement; -use PHPUnit\Framework\TestCase; +use Tests\Form\Attribute\TestCase; class ReflectionProcessorTest extends TestCase { @@ -63,9 +63,6 @@ public function test_should_not_configure_twice_same_button_property() class A extends AttributeForm { - public $withoutType; - public array $withNotObjectType; - public StringElement $foo; protected ButtonInterface $btn; @@ -75,6 +72,9 @@ class A extends AttributeForm class B extends A { + public $withoutType; + public array $withNotObjectType; + public StringElement $foo; protected ButtonInterface $btn; } diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..4fe0fca --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,37 @@ + [new ReflectionProcessor(new ConfigureFormBuilderStrategy())], + 'compile' => [new CompileAttributesProcessor( + 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'); + $processor = new ReflectionProcessor($generator); + + $processor->configureBuilder($form, new FormBuilder()); + $this->assertEquals($expected, $generator->code()); + } +} From 42997aedbe71b4a26eb39571a2f5b3211a1653c8 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 8 Dec 2021 15:45:22 +0100 Subject: [PATCH 10/40] Add new attributes and solve TODOs --- composer.json | 3 +- phpunit.xml.dist | 2 +- src/Aggregate/ArrayConstraint.php | 3 +- src/Aggregate/CallbackArrayConstraint.php | 104 +++++++++++++ src/Child/CallbackModelTransformer.php | 34 +---- ...mConstraint.php => CallbackConstraint.php} | 4 +- src/Element/CallbackTransformer.php | 20 +-- src/Element/Date/DateFormat.php | 63 ++++++++ src/Element/Date/DateTimeClass.php | 63 ++++++++ src/Element/Date/ImmutableDateTime.php | 53 +++++++ src/Element/Date/Timezone.php | 62 ++++++++ src/Element/Raw.php | 61 ++++++++ src/Form/Csrf.php | 121 +++++++++++++++ .../AttributesProcessorGenerator.php | 11 -- .../TransformerClassGenerator.php | 77 ++++++++++ src/Processor/CompileAttributesProcessor.php | 21 ++- .../ConfigureFormBuilderStrategy.php | 28 ++-- .../Element/ConstraintAttributeProcessor.php | 12 +- .../Element/ExtractorAttributeProcessor.php | 10 +- .../Element/FilterAttributeProcessor.php | 12 +- .../Element/HydratorAttributeProcessor.php | 12 +- .../SimpleMethodCallGeneratorTrait.php | 32 ++++ .../Element/TransformerAttributeProcessor.php | 12 +- .../GenerateConfiguratorStrategy.php | 28 ++-- .../Aggregate/CallbackArrayConstraintTest.php | 108 ++++++++++++++ tests/Child/CallbackModelTransformerTest.php | 22 +-- tests/Child/ModelTransformerTest.php | 1 - ...intTest.php => CallbackConstraintTest.php} | 10 +- tests/Element/CallbackTransformerTest.php | 11 +- tests/Element/Date/DateFormatTest.php | 71 +++++++++ tests/Element/Date/DateTimeClassTest.php | 79 ++++++++++ tests/Element/Date/ImmtableDateTimeTest.php | 76 ++++++++++ tests/Element/Date/TimezoneTest.php | 75 ++++++++++ tests/Element/RawTest.php | 93 ++++++++++++ tests/Form/CsrfTest.php | 132 +++++++++++++++++ tests/Form/GeneratesTest.php | 1 - .../TransformerClassGeneratorTest.php | 138 ++++++++++++++++++ .../CompileAttributesProcessorTest.php | 89 ++++++++++- .../ConfigureFormBuilderStrategyTest.php | 46 ------ .../TransformerAttributeProcessorTest.php | 4 +- tests/bootstrap.php | 5 + 41 files changed, 1605 insertions(+), 204 deletions(-) create mode 100644 src/Aggregate/CallbackArrayConstraint.php rename src/Constraint/{CustomConstraint.php => CallbackConstraint.php} (96%) create mode 100644 src/Element/Date/DateFormat.php create mode 100644 src/Element/Date/DateTimeClass.php create mode 100644 src/Element/Date/ImmutableDateTime.php create mode 100644 src/Element/Date/Timezone.php create mode 100644 src/Element/Raw.php create mode 100644 src/Form/Csrf.php create mode 100644 src/Processor/CodeGenerator/TransformerClassGenerator.php create mode 100644 src/Processor/Element/SimpleMethodCallGeneratorTrait.php create mode 100644 tests/Aggregate/CallbackArrayConstraintTest.php rename tests/Constraint/{CustomConstraintTest.php => CallbackConstraintTest.php} (88%) create mode 100644 tests/Element/Date/DateFormatTest.php create mode 100644 tests/Element/Date/DateTimeClassTest.php create mode 100644 tests/Element/Date/ImmtableDateTimeTest.php create mode 100644 tests/Element/Date/TimezoneTest.php create mode 100644 tests/Element/RawTest.php create mode 100644 tests/Form/CsrfTest.php create mode 100644 tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php delete mode 100644 tests/Processor/ConfigureFormBuilderStrategyTest.php create mode 100644 tests/bootstrap.php diff --git a/composer.json b/composer.json index 924a01d..e842942 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "require-dev": { "phpunit/phpunit": "~9.5", "vimeo/psalm": "dev-master", - "squizlabs/php_codesniffer": "~3.6.1" + "squizlabs/php_codesniffer": "~3.6.1", + "symfony/security-csrf": "~6.1" }, "scripts": { "tests": "phpunit", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7e7e2e5..a9e870d 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ array('values')->arrayConstraints(MyConstraint::class, $options); * * - * @todo custom array constraint - * * Usage: * * class MyForm extends AttributeForm @@ -35,6 +33,7 @@ * * @see Satisfy Attribute for add constraint for items * @see ArrayElementBuilder::arrayConstraint() The called method + * @see CallbackArrayConstraint Use for a custom method validation * * @implements ChildBuilderAttributeInterface */ diff --git a/src/Aggregate/CallbackArrayConstraint.php b/src/Aggregate/CallbackArrayConstraint.php new file mode 100644 index 0000000..7e9de3f --- /dev/null +++ b/src/Aggregate/CallbackArrayConstraint.php @@ -0,0 +1,104 @@ + + * $builder->array('foo')->arrayConstraint([$this, 'validateFoo'], 'Foo is invalid'); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[CustomConstraint('validateFoo', message: 'Foo is invalid')] + * private ArrayElement $foo; + * + * public function validateFoo(array $value, ElementInterface $input): bool + * { + * return count($value) % 5 > 2; + * } + * } + * + * + * @see ArrayElementBuilder::arrayConstraint() The called method + * @see Constraint + * @see Closure The used constraint + * @see ArrayConstraint Use for a class constraint + * + * @implements ChildBuilderAttributeInterface + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class CallbackArrayConstraint implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The method name to use as validator + * Must be a public method declared on the form class + * + * Its prototype should be : + * `public function (array $value, ElementInterface $input): bool|string|array{code: string message: string}|null` + * + * - Return true, or null (nothing) for a valid input + * - Return false for invalid input, with the default error message (or the declared one) + * - Return string for a custom error message + * - Return array with error message and code + * + * @var literal-string + */ + public string $methodName, + /** + * The error message to use + * This option is used only if the validator return false, in other cases, + * the message returned by the validator will be used + * + * @var string|null + */ + public ?string $message = null, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $constraint = new Closure(['callback' => [$form, $this->methodName]]); + + if ($this->message) { + $constraint->message = $this->message; + } + + $builder->arrayConstraint($constraint); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->use(Closure::class, 'ClosureConstraint'); + + $parameters = $this->message + ? new Literal("['callback' => [\$form, ?], 'message' => ?]", [$this->methodName, $this->message]) + : new Literal('[$form, ?]', [$this->methodName]) + ; + + $generator->line('$?->arrayConstraint(new ClosureConstraint(?));', [$name, $parameters]); + } +} diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index 183aae2..6497b0b 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -7,6 +7,7 @@ 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; @@ -137,54 +138,35 @@ public function transformFromHttp($value, ElementInterface $input) }); } - /** * {@inheritdoc} */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - // @todo refactor with CallbackTransformer if ($this->callback) { $generator->line('$?->modelTransformer([$form, ?]);', [$name, $this->callback]); return; } - $generator->use(TransformerInterface::class); - $generator->use(ElementInterface::class); - - $transformer = new ClassType(); - $transformer->addImplement(TransformerInterface::class); - - $transformer->addProperty('form'); - - $constructor = $transformer->addMethod('__construct'); - $constructor->addParameter('form'); - $constructor->setBody('$this->form = $form;'); - - $toInput = Method::from([TransformerInterface::class, 'transformToHttp']); - $toEntity = Method::from([TransformerInterface::class, 'transformFromHttp']); - - $toInput->setComment('{@inheritdoc}'); - $toEntity->setComment('{@inheritdoc}'); + $transformer = new TransformerClassGenerator($generator->namespace(), $generator->printer()); - $transformer->addMember($toInput); - $transformer->addMember($toEntity); + $transformer->withPromotedProperty('form')->setPrivate(); if ($this->toInput) { - $toInput->setBody('return $this->form->?($value, $input);', [$this->toInput]); + $transformer->toHttp()->setBody('return $this->form->?($value, $input);', [$this->toInput]); } else { - $toInput->setBody('return $value;'); + $transformer->toHttp()->setBody('return $value;'); } if ($this->toEntity) { - $toEntity->setBody('return $this->form->?($value, $input);', [$this->toEntity]); + $transformer->fromHttp()->setBody('return $this->form->?($value, $input);', [$this->toEntity]); } else { - $toEntity->setBody('return $value;'); + $transformer->fromHttp()->setBody('return $value;'); } $generator->line( '$?->modelTransformer(new class ($form) ?);', - [$name, new Literal((new PsrPrinter())->printClass($transformer, $generator->namespace()))] + [$name, new Literal($transformer->generateClass())] ); } } diff --git a/src/Constraint/CustomConstraint.php b/src/Constraint/CallbackConstraint.php similarity index 96% rename from src/Constraint/CustomConstraint.php rename to src/Constraint/CallbackConstraint.php index b28c8dd..d6108a0 100644 --- a/src/Constraint/CustomConstraint.php +++ b/src/Constraint/CallbackConstraint.php @@ -40,11 +40,9 @@ * @see ElementBuilderInterface::satisfy() The called method * @see Constraint * @see Closure The used constraint - * - * @todo rename callback constraint */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CustomConstraint implements ChildBuilderAttributeInterface +final class CallbackConstraint implements ChildBuilderAttributeInterface { public function __construct( /** diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index 9f122bc..cd1f99a 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\CodeGenerator\ClassGenerator; +use Bdf\Form\Attribute\Processor\CodeGenerator\TransformerClassGenerator; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; @@ -150,27 +151,20 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen return; } - $transformer = $generator->anonymousClass(); - $transformer->implements(TransformerInterface::class); + $transformer = new TransformerClassGenerator($generator->namespace(), $generator->printer()); - $transformer->class()->addProperty('form'); - $constructor = $transformer->class()->addMethod('__construct'); - $constructor->addParameter('form'); - $constructor->setBody('$this->form = $form;'); - - $toHttp = $transformer->implementsMethod(TransformerInterface::class, 'transformToHttp'); - $fromHttp = $transformer->implementsMethod(TransformerInterface::class, 'transformFromHttp'); + $transformer->withPromotedProperty('form')->setPrivate(); if ($this->toHttp) { - $toHttp->setBody('return $this->form->?($value, $input);', [$this->toHttp]); + $transformer->toHttp()->setBody('return $this->form->?($value, $input);', [$this->toHttp]); } else { - $toHttp->setBody('return $value;'); + $transformer->toHttp()->setBody('return $value;'); } if ($this->fromHttp) { - $fromHttp->setBody('return $this->form->?($value, $input);', [$this->fromHttp]); + $transformer->fromHttp()->setBody('return $this->form->?($value, $input);', [$this->fromHttp]); } else { - $fromHttp->setBody('return $value;'); + $transformer->fromHttp()->setBody('return $value;'); } $generator->line( diff --git a/src/Element/Date/DateFormat.php b/src/Element/Date/DateFormat.php new file mode 100644 index 0000000..375e3d8 --- /dev/null +++ b/src/Element/Date/DateFormat.php @@ -0,0 +1,63 @@ + + * $builder->dateTime('date')->format('d/m/Y H:i'); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[DateFormat('d/m/Y H:i')] + * private DateTimeElement $foo; + * } + * + * + * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::format() The called method + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class DateFormat implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The date format + * + * @var non-empty-string + * @see https://www.php.net/manual/en/datetime.createfromformat.php#refsect1-datetime.createfromformat-parameters For the format + */ + public string $format, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->format($this->format); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->format(?);', [$name, $this->format]); + } +} diff --git a/src/Element/Date/DateTimeClass.php b/src/Element/Date/DateTimeClass.php new file mode 100644 index 0000000..f5615af --- /dev/null +++ b/src/Element/Date/DateTimeClass.php @@ -0,0 +1,63 @@ + + * $builder->dateTime('date')->className(Carbon::class); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[DateTimeClass(Carbon::class)] + * private DateTimeElement $foo; + * } + * + * + * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::className() The called method + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class DateTimeClass implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The datetime class to use + * + * @var class-string<\DateTimeInterface> + */ + public string $className, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->className($this->className); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->className(?::class);', [$name, new Literal($generator->useAndSimplifyType($this->className))]); + } +} diff --git a/src/Element/Date/ImmutableDateTime.php b/src/Element/Date/ImmutableDateTime.php new file mode 100644 index 0000000..c99bfe9 --- /dev/null +++ b/src/Element/Date/ImmutableDateTime.php @@ -0,0 +1,53 @@ + + * $builder->dateTime('date')->immutable(); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[ImmutableDateTime] + * private DateTimeElement $foo; + * } + * + * + * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::immutable() The called method + * @see DateTimeClass For use a custom class name + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class ImmutableDateTime implements ChildBuilderAttributeInterface +{ + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->immutable(); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->immutable();', [$name]); + } +} diff --git a/src/Element/Date/Timezone.php b/src/Element/Date/Timezone.php new file mode 100644 index 0000000..45a44ed --- /dev/null +++ b/src/Element/Date/Timezone.php @@ -0,0 +1,62 @@ + + * $builder->dateTime('date')->timezone('Europe/Paris'); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[Timezone('Europe/Paris')] + * private DateTimeElement $foo; + * } + * + * + * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::timezone() The called method + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Timezone implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The timezone name or offset + * + * @var non-empty-string + */ + public string $timezone, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->timezone($this->timezone); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->timezone(?);', [$name, $this->timezone]); + } +} diff --git a/src/Element/Raw.php b/src/Element/Raw.php new file mode 100644 index 0000000..7fc5139 --- /dev/null +++ b/src/Element/Raw.php @@ -0,0 +1,61 @@ + + * $builder->float('foo')->raw(); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[Raw] + * private IntegerElement $foo; + * } + * + * + * @see NumberElementBuilder::raw() The called method + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\NumberElementBuilder> + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Raw implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * Enable or disable raw mode for parsing numbers + */ + public bool $flag = true, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->raw($this->flag); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->raw(?);', [$name, $this->flag]); + } +} diff --git a/src/Form/Csrf.php b/src/Form/Csrf.php new file mode 100644 index 0000000..1b65b79 --- /dev/null +++ b/src/Form/Csrf.php @@ -0,0 +1,121 @@ + + * $builder->csrf('csrf')->message('Token invalide'); + * + * + * Usage: + * + * #[Csrf(name: 'csrf', message: 'Token invalide')] + * class MyForm extends AttributeForm + * { + * } + * + * + * @see FormBuilderInterface::csrf() The called method + * @see CsrfElementBuilder + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class Csrf implements FormBuilderAttributeInterface +{ + public function __construct( + /** + * The token input name + * + * @var non-empty-string + */ + public string $name = '_token', + /** + * The token id + * By default is value is the class name of `CsrfElement` + * + * @var string|null + * + * @see CsrfTokenManagerInterface::getToken() The parameter tokenId will be used as parameter of this method + * @see CsrfElementBuilder::tokenId() The called method if defined + */ + public ?string $tokenId = null, + /** + * The error message to display if the token do not correspond + * + * @var string|null + * + * @see CsrfElementBuilder::message() The called method if defined + */ + public ?string $message = null, + /** + * Always invalidate the CSRF token after submission + * + * @var bool|null + * + * @see CsrfElementBuilder::invalidate() The called method if defined + */ + public ?bool $invalidate = null, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void + { + $csrf = $builder->csrf($this->name); + + if ($this->tokenId !== null) { + $csrf->tokenId($this->tokenId); + } + + if ($this->message !== null) { + $csrf->message($this->message); + } + + if ($this->invalidate !== null) { + $csrf->invalidate($this->invalidate); + } + } + + /** + * {@inheritdoc} + */ + public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $parameters = [$this->name]; + $line = '$builder->csrf(?)'; + + if ($this->tokenId !== null) { + $line .= '->tokenId(?)'; + $parameters[] = $this->tokenId; + } + + if ($this->message !== null) { + $line .= '->message(?)'; + $parameters[] = $this->message; + } + + if ($this->invalidate !== null) { + $line .= '->invalidate(?)'; + $parameters[] = $this->invalidate; + } + + $line .= ';'; + + $generator->line($line, $parameters); + } +} diff --git a/src/Processor/CodeGenerator/AttributesProcessorGenerator.php b/src/Processor/CodeGenerator/AttributesProcessorGenerator.php index 0d5801d..b0ab9a0 100644 --- a/src/Processor/CodeGenerator/AttributesProcessorGenerator.php +++ b/src/Processor/CodeGenerator/AttributesProcessorGenerator.php @@ -4,7 +4,6 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use InvalidArgumentException; -use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\Method; use Nette\PhpGenerator\PhpNamespace; @@ -67,16 +66,6 @@ public function new(string $className, array $parameters, ?string $classAlias = return new Literal('new ?(...?:)', [new Literal($className), $parameters]); } - /** - * Get utility for generate an anonymous class - * - * @return ClassGenerator - */ - public function anonymousClass(): ClassGenerator - { - return new ClassGenerator($this->namespace(), new ClassType(), $this->printer()); - } - /** * Print the class code * diff --git a/src/Processor/CodeGenerator/TransformerClassGenerator.php b/src/Processor/CodeGenerator/TransformerClassGenerator.php new file mode 100644 index 0000000..f0e31f8 --- /dev/null +++ b/src/Processor/CodeGenerator/TransformerClassGenerator.php @@ -0,0 +1,77 @@ +implements(TransformerInterface::class); + + $this->toHttp = $this->implementsMethod(TransformerInterface::class, 'transformToHttp'); + $this->fromHttp = $this->implementsMethod(TransformerInterface::class, 'transformFromHttp'); + } + + /** + * Add a new promoted property on constructor + * + * @param string $name The property name, without $ + * + * @return PromotedParameter + */ + public function withPromotedProperty(string $name): PromotedParameter + { + if (!$this->constructor) { + $this->constructor = $this->class()->addMethod('__construct'); + } + + return $this->constructor->addPromotedParameter($name); + } + + /** + * Get the transformToHttp method builder + * + * @return Method + * + * @see TransformerInterface::transformToHttp() + */ + public function toHttp(): Method + { + return $this->toHttp; + } + + /** + * Get the transformFromHttp method builder + * + * @return Method + * + * @see TransformerInterface::transformFromHttp() + */ + public function fromHttp(): Method + { + return $this->fromHttp; + } +} diff --git a/src/Processor/CompileAttributesProcessor.php b/src/Processor/CompileAttributesProcessor.php index b64566c..d4cfb48 100644 --- a/src/Processor/CompileAttributesProcessor.php +++ b/src/Processor/CompileAttributesProcessor.php @@ -2,6 +2,7 @@ namespace Bdf\Form\Attribute\Processor; +use Bdf\Form\Aggregate\FormBuilder; use Bdf\Form\Aggregate\FormBuilderInterface; use Bdf\Form\Attribute\AttributeForm; use LogicException; @@ -15,8 +16,6 @@ * - Include the processor class file * - Instantiate the generated processor * - Delegate the form configuration to the generated processor - * - * @todo public method for only generate the class */ final class CompileAttributesProcessor implements AttributesProcessorInterface { @@ -57,6 +56,24 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil return $generated; } + /** + * Generate the configurator for the given form + * Unlike `configureBuilder()` process, the class will be regenerated if already exists, + * and the class will not be included + * + * @param AttributeForm $form Form to generate + * + * @return void + */ + public function generate(AttributeForm $form): void + { + /** @var class-string $className */ + $className = ($this->classNameResolver)($form); + $fileName = ($this->fileNameResolver)($className); + + $this->generateProcessor($fileName, $className, $form, new FormBuilder()); + } + /** * Try to load the processor from its file * diff --git a/src/Processor/ConfigureFormBuilderStrategy.php b/src/Processor/ConfigureFormBuilderStrategy.php index ee8aa9a..189bbd3 100644 --- a/src/Processor/ConfigureFormBuilderStrategy.php +++ b/src/Processor/ConfigureFormBuilderStrategy.php @@ -36,20 +36,6 @@ public function __construct() $this->registerElementAttributeProcessor(new ExtractorAttributeProcessor()); } - /** - * Register a new processor for element attributes - * - * @param ElementAttributeProcessorInterface $processor - * - * @return void - * - * @template T as object - */ - public function registerElementAttributeProcessor(ElementAttributeProcessorInterface $processor): void - { - $this->elementProcessors[] = $processor; - } - /** * {@inheritdoc} */ @@ -99,4 +85,18 @@ public function onPostConfigure(array $elementProperties, array $buttonPropertie { return new PostConfigureReflectionSetProperties($elementProperties, $buttonProperties); } + + /** + * 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/Processor/Element/ConstraintAttributeProcessor.php b/src/Processor/Element/ConstraintAttributeProcessor.php index 52a6470..302a690 100644 --- a/src/Processor/Element/ConstraintAttributeProcessor.php +++ b/src/Processor/Element/ConstraintAttributeProcessor.php @@ -2,12 +2,8 @@ namespace Bdf\Form\Attribute\Processor\Element; -use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; -use Nette\PhpGenerator\Literal; -use ReflectionAttribute; use Symfony\Component\Validator\Constraint; /** @@ -20,6 +16,8 @@ */ final class ConstraintAttributeProcessor implements ElementAttributeProcessorInterface { + use SimpleMethodCallGeneratorTrait; + /** * {@inheritdoc} */ @@ -39,10 +37,8 @@ public function process(ChildBuilderInterface $builder, object $attribute): void /** * {@inheritdoc} */ - public function generateCode(string $name, AttributesProcessorGenerator $generator, ReflectionAttribute $attribute): void + private function methodName(): string { - /** @var class-string $constraint */ - $constraint = $attribute->getName(); - $generator->line('$?->satisfy(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + return 'satisfy'; } } diff --git a/src/Processor/Element/ExtractorAttributeProcessor.php b/src/Processor/Element/ExtractorAttributeProcessor.php index 9a25597..6e9ed3d 100644 --- a/src/Processor/Element/ExtractorAttributeProcessor.php +++ b/src/Processor/Element/ExtractorAttributeProcessor.php @@ -2,7 +2,6 @@ namespace Bdf\Form\Attribute\Processor\Element; -use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\PropertyAccess\ExtractorInterface; @@ -16,6 +15,8 @@ */ final class ExtractorAttributeProcessor implements ElementAttributeProcessorInterface { + use SimpleMethodCallGeneratorTrait; + /** * {@inheritdoc} */ @@ -35,11 +36,8 @@ public function process(ChildBuilderInterface $builder, object $attribute): void /** * {@inheritdoc} */ - public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + private function methodName(): string { - // @todo refactor - /** @var class-string $constraint */ - $constraint = $attribute->getName(); - $generator->line('$?->extractor(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + return 'extractor'; } } diff --git a/src/Processor/Element/FilterAttributeProcessor.php b/src/Processor/Element/FilterAttributeProcessor.php index 033b10d..d4bad1f 100644 --- a/src/Processor/Element/FilterAttributeProcessor.php +++ b/src/Processor/Element/FilterAttributeProcessor.php @@ -2,11 +2,8 @@ namespace Bdf\Form\Attribute\Processor\Element; -use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Filter\FilterInterface; -use Nette\PhpGenerator\Literal; /** * Add the filter by calling filter() @@ -18,6 +15,8 @@ */ final class FilterAttributeProcessor implements ElementAttributeProcessorInterface { + use SimpleMethodCallGeneratorTrait; + /** * {@inheritdoc} */ @@ -37,11 +36,8 @@ public function process(ChildBuilderInterface $builder, object $attribute): void /** * {@inheritdoc} */ - public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + private function methodName(): string { - // @todo refactor - /** @var class-string $constraint */ - $constraint = $attribute->getName(); - $generator->line('$?->filter(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + return 'filter'; } } diff --git a/src/Processor/Element/HydratorAttributeProcessor.php b/src/Processor/Element/HydratorAttributeProcessor.php index f684f07..722e0a7 100644 --- a/src/Processor/Element/HydratorAttributeProcessor.php +++ b/src/Processor/Element/HydratorAttributeProcessor.php @@ -2,11 +2,8 @@ namespace Bdf\Form\Attribute\Processor\Element; -use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\PropertyAccess\HydratorInterface; -use Nette\PhpGenerator\Literal; /** * Define as hydrator by calling hydrator() @@ -18,6 +15,8 @@ */ final class HydratorAttributeProcessor implements ElementAttributeProcessorInterface { + use SimpleMethodCallGeneratorTrait; + /** * {@inheritdoc} */ @@ -37,11 +36,8 @@ public function process(ChildBuilderInterface $builder, object $attribute): void /** * {@inheritdoc} */ - public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + private function methodName(): string { - // @todo refactor - /** @var class-string $constraint */ - $constraint = $attribute->getName(); - $generator->line('$?->hydrator(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + return 'hydrator'; } } diff --git a/src/Processor/Element/SimpleMethodCallGeneratorTrait.php b/src/Processor/Element/SimpleMethodCallGeneratorTrait.php new file mode 100644 index 0000000..7930a1f --- /dev/null +++ b/src/Processor/Element/SimpleMethodCallGeneratorTrait.php @@ -0,0 +1,32 @@ +getName(); + $generator->line('$?->?(?);', [$name, $this->methodName(), $generator->new($constraint, $attribute->getArguments())]); + } + + /** + * Called method name + * + * @return literal-string + */ + abstract private function methodName(): string; +} diff --git a/src/Processor/Element/TransformerAttributeProcessor.php b/src/Processor/Element/TransformerAttributeProcessor.php index 719e953..dc3d386 100644 --- a/src/Processor/Element/TransformerAttributeProcessor.php +++ b/src/Processor/Element/TransformerAttributeProcessor.php @@ -2,12 +2,9 @@ namespace Bdf\Form\Attribute\Processor\Element; -use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; -use Nette\PhpGenerator\Literal; /** * Add the transformer by calling transformer() @@ -19,6 +16,8 @@ */ final class TransformerAttributeProcessor implements ElementAttributeProcessorInterface { + use SimpleMethodCallGeneratorTrait; + /** * {@inheritdoc} */ @@ -38,11 +37,8 @@ public function process(ChildBuilderInterface $builder, object $attribute): void /** * {@inheritdoc} */ - public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void + private function methodName(): string { - // @todo refactor - /** @var class-string $constraint */ - $constraint = $attribute->getName(); - $generator->line('$?->transformer(?);', [$name, $generator->new($constraint, $attribute->getArguments())]); + return 'transformer'; } } diff --git a/src/Processor/GenerateConfiguratorStrategy.php b/src/Processor/GenerateConfiguratorStrategy.php index d991ebf..d9f86d4 100644 --- a/src/Processor/GenerateConfiguratorStrategy.php +++ b/src/Processor/GenerateConfiguratorStrategy.php @@ -47,20 +47,6 @@ public function __construct(string $className) $this->registerElementAttributeProcessor(new ExtractorAttributeProcessor()); } - /** - * Register a new processor for element attributes - * - * @param ElementAttributeProcessorInterface $processor - * - * @return void - * - * @template T as object - */ - public function registerElementAttributeProcessor(ElementAttributeProcessorInterface $processor): void - { - $this->elementProcessors[] = $processor; - } - /** * {@inheritdoc} */ @@ -178,4 +164,18 @@ 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/tests/Aggregate/CallbackArrayConstraintTest.php b/tests/Aggregate/CallbackArrayConstraintTest.php new file mode 100644 index 0000000..678b9c1 --- /dev/null +++ b/tests/Aggregate/CallbackArrayConstraintTest.php @@ -0,0 +1,108 @@ +submit(['foo' => ['a']]); + + $this->assertFalse($form->valid()); + $this->assertEquals('Foo size must be a multiple of 2', $form->foo->error()->global()); + + $form->submit(['foo' => ['a', 'b']]); + + $this->assertTrue($form->valid()); + $this->assertNull($form->foo->error()->global()); + + $form->submit(['bar' => ['a']]); + + $this->assertFalse($form->valid()); + $this->assertEquals('The value is invalid', $form->bar->error()->global()); + + $form->submit(['bar' => ['a', 'b']]); + + $this->assertTrue($form->valid()); + $this->assertNull($form->bar->error()->global()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[CallbackArrayConstraint('validateFoo', message: 'Foo size must be a multiple of 2')] + public ArrayElement $foo; + + #[CallbackArrayConstraint('validateFoo')] + public ArrayElement $bar; + + public function validateFoo(array $value): bool + { + return count($value) % 2 === 0; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Constraint\Closure as ClosureConstraint; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', ArrayElement::class); + $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2'])); + + $bar = $builder->add('bar', ArrayElement::class); + $bar->arrayConstraint(new ClosureConstraint([$form, 'validateFoo'])); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Child/CallbackModelTransformerTest.php b/tests/Child/CallbackModelTransformerTest.php index 0469288..cb0cc42 100644 --- a/tests/Child/CallbackModelTransformerTest.php +++ b/tests/Child/CallbackModelTransformerTest.php @@ -117,13 +117,6 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? { $foo = $builder->add('foo', IntegerElement::class); $foo->modelTransformer(new class ($form) implements TransformerInterface { - public $form; - - public function __construct($form) - { - $this->form = $form; - } - /** * {@inheritdoc} */ @@ -139,19 +132,16 @@ function transformFromHttp($value, ElementInterface $input) { return $this->form->t($value, $input); } + + public function __construct(private $form) + { + } }); $foo->hydrator(new Setter()); $foo->extractor(new Getter()); $bar = $builder->add('bar', IntegerElement::class); $bar->modelTransformer(new class ($form) implements TransformerInterface { - public $form; - - public function __construct($form) - { - $this->form = $form; - } - /** * {@inheritdoc} */ @@ -167,6 +157,10 @@ function transformFromHttp($value, ElementInterface $input) { return $value; } + + public function __construct(private $form) + { + } }); $bar->hydrator(new Setter()); $bar->extractor(new Getter()); diff --git a/tests/Child/ModelTransformerTest.php b/tests/Child/ModelTransformerTest.php index 9e99171..07db1da 100644 --- a/tests/Child/ModelTransformerTest.php +++ b/tests/Child/ModelTransformerTest.php @@ -15,7 +15,6 @@ use Bdf\Form\Transformer\TransformerInterface; use Tests\Form\Attribute\TestCase; -// @todo Raw attribute for number elements class ModelTransformerTest extends TestCase { /** diff --git a/tests/Constraint/CustomConstraintTest.php b/tests/Constraint/CallbackConstraintTest.php similarity index 88% rename from tests/Constraint/CustomConstraintTest.php rename to tests/Constraint/CallbackConstraintTest.php index 19058c2..8897442 100644 --- a/tests/Constraint/CustomConstraintTest.php +++ b/tests/Constraint/CallbackConstraintTest.php @@ -3,12 +3,12 @@ namespace Tests\Form\Attribute\Constraint; use Bdf\Form\Attribute\AttributeForm; -use Bdf\Form\Attribute\Constraint\CustomConstraint; +use Bdf\Form\Attribute\Constraint\CallbackConstraint; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; use Tests\Form\Attribute\TestCase; -class CustomConstraintTest extends TestCase +class CallbackConstraintTest extends TestCase { /** * @dataProvider provideAttributesProcessor @@ -16,10 +16,10 @@ class CustomConstraintTest extends TestCase public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { - #[CustomConstraint('validateFoo', message: 'Foo length must be a multiple of 2')] + #[CallbackConstraint('validateFoo', message: 'Foo length must be a multiple of 2')] public StringElement $foo; - #[CustomConstraint('validateFoo')] + #[CallbackConstraint('validateFoo')] public StringElement $bar; public function validateFoo($value): bool @@ -52,7 +52,7 @@ public function validateFoo($value): bool public function test_code_generator() { $form = new class extends AttributeForm { - #[CustomConstraint('validateFoo', message: 'Foo length must be a multiple of 2')] + #[CallbackConstraint('validateFoo', message: 'Foo length must be a multiple of 2')] public StringElement $foo; public function validateFoo($value): bool diff --git a/tests/Element/CallbackTransformerTest.php b/tests/Element/CallbackTransformerTest.php index 24021e0..26849fb 100644 --- a/tests/Element/CallbackTransformerTest.php +++ b/tests/Element/CallbackTransformerTest.php @@ -129,13 +129,6 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $bar = $builder->add('bar', StringElement::class); $bar->transformer(new class ($form) implements TransformerInterface { - public $form; - - public function __construct($form) - { - $this->form = $form; - } - /** * {@inheritdoc} */ @@ -151,6 +144,10 @@ function transformFromHttp($value, ElementInterface $input) { return $this->form->inTransformer($value, $input); } + + public function __construct(private $form) + { + } }); return $this; diff --git a/tests/Element/Date/DateFormatTest.php b/tests/Element/Date/DateFormatTest.php new file mode 100644 index 0000000..58c1eaf --- /dev/null +++ b/tests/Element/Date/DateFormatTest.php @@ -0,0 +1,71 @@ +submit(['foo' => '02/11/2020 15:21']); + + $this->assertEquals(new MyCustomDate('2020-11-02T15:21:00'), $form->foo->value()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[DateFormat('d/m/Y H:i')] + public DateTimeElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\Date\DateTimeElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', DateTimeElement::class); + $foo->format('d/m/Y H:i'); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Element/Date/DateTimeClassTest.php b/tests/Element/Date/DateTimeClassTest.php new file mode 100644 index 0000000..aea5039 --- /dev/null +++ b/tests/Element/Date/DateTimeClassTest.php @@ -0,0 +1,79 @@ +submit(['foo' => '2020-11-02T15:21:31+0000']); + + $this->assertEquals(new MyCustomDate('2020-11-02T15:21:31'), $form->foo->value()); + $this->assertInstanceOf(MyCustomDate::class, $form->foo->value()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[DateTimeClass(MyCustomDate::class)] + public DateTimeElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\Date\DateTimeElement; +use Tests\Form\Attribute\Element\Date\MyCustomDate; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', DateTimeElement::class); + $foo->className(MyCustomDate::class); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form + ); + } +} + +class MyCustomDate extends \DateTime +{ +} diff --git a/tests/Element/Date/ImmtableDateTimeTest.php b/tests/Element/Date/ImmtableDateTimeTest.php new file mode 100644 index 0000000..4b38689 --- /dev/null +++ b/tests/Element/Date/ImmtableDateTimeTest.php @@ -0,0 +1,76 @@ +submit(['foo' => '2020-11-02T15:21:31+0000']); + + $this->assertEquals(new DateTimeImmutable('2020-11-02T15:21:31'), $form->foo->value()); + $this->assertInstanceOf(DateTimeImmutable::class, $form->foo->value()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[ImmutableDateTime] + public DateTimeElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\Date\DateTimeElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', DateTimeElement::class); + $foo->immutable(); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Element/Date/TimezoneTest.php b/tests/Element/Date/TimezoneTest.php new file mode 100644 index 0000000..ed4ddb9 --- /dev/null +++ b/tests/Element/Date/TimezoneTest.php @@ -0,0 +1,75 @@ +submit(['foo' => '2020-11-02T15:21:00+0200']); + + $this->assertEquals(new DateTime('2020-11-02T18:21:00+0500'), $form->foo->value()); + $this->assertEquals(new \DateTimeZone('+0500'), $form->foo->value()->getTimezone()); + $this->assertEquals(5 * 3600, $form->foo->value()->getOffset()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Timezone('+0500')] + public DateTimeElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\Date\DateTimeElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', DateTimeElement::class); + $foo->timezone('+0500'); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Element/RawTest.php b/tests/Element/RawTest.php new file mode 100644 index 0000000..5a223c4 --- /dev/null +++ b/tests/Element/RawTest.php @@ -0,0 +1,93 @@ +lastLocale = \Locale::getDefault(); + \Locale::setDefault('FR_fr'); + } + + protected function tearDown(): void + { + \Locale::setDefault($this->lastLocale); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { + #[Raw] + public FloatElement $foo; + #[Raw(false)] + public FloatElement $bar; + }; + + $form->submit(['foo' => '1,23', 'bar' => '1,23']); + + $this->assertSame(1.0, $form->foo->value()); + $this->assertSame(1.23, $form->bar->value()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Raw] + public FloatElement $foo; + #[Raw(false)] + public FloatElement $bar; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\FloatElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', FloatElement::class); + $foo->raw(true); + + $bar = $builder->add('bar', FloatElement::class); + $bar->raw(false); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Form/CsrfTest.php b/tests/Form/CsrfTest.php new file mode 100644 index 0000000..1f03add --- /dev/null +++ b/tests/Form/CsrfTest.php @@ -0,0 +1,132 @@ +submit([]); + $this->assertFalse($form->valid()); + $this->assertEquals(['_token' => 'The CSRF token is invalid.'], $form->error()->toArray()); + + $form->submit(['_token' => $form['_token']->view()->value()]); + $this->assertTrue($form->valid()); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_message(AttributesProcessorInterface $processor) + { + $form = new #[Csrf(message: 'my error')] class(null, $processor) extends AttributeForm { + }; + + $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_name(AttributesProcessorInterface $processor) + { + $form = new #[Csrf(name: 't')] class(null, $processor) extends AttributeForm { + }; + + $form->submit([]); + $this->assertFalse($form->valid()); + $this->assertEquals(['t' => 'The CSRF token is invalid.'], $form->error()->toArray()); + + $form->submit(['t' => $form['t']->view()->value()]); + $this->assertTrue($form->valid()); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_invalidate(AttributesProcessorInterface $processor) + { + $form = new #[Csrf(invalidate: true)] class(null, $processor) extends AttributeForm { + }; + + $token = $form['_token']->view()->value(); + + $form->submit(['_token' => $token]); + $this->assertTrue($form->valid()); + + $form->submit(['_token' => $token]); + $this->assertFalse($form->valid()); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_tokenId(AttributesProcessorInterface $processor) + { + $form = new #[Csrf(tokenId: 'my_token_id')] class(null, $processor) extends AttributeForm { + }; + + $token = $form['_token']->view()->value(); + + $this->assertSame('my_token_id', $token->getId()); + } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new #[Csrf(tokenId: 'my_token', message: 'my error', invalidate: true)] class extends AttributeForm { + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $builder->csrf('_token')->tokenId('my_token')->message('my error')->invalidate(true); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + } +} + +PHP + , $form); + } +} diff --git a/tests/Form/GeneratesTest.php b/tests/Form/GeneratesTest.php index beff772..78adc47 100644 --- a/tests/Form/GeneratesTest.php +++ b/tests/Form/GeneratesTest.php @@ -105,7 +105,6 @@ protected function configure(FormBuilderInterface $builder): void } } -// @todo is repetable class OrderForm extends AttributeForm { #[Positive(message: 'Valeur incorrecte'), Getter, Setter] diff --git a/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php b/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php new file mode 100644 index 0000000..fd98c49 --- /dev/null +++ b/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php @@ -0,0 +1,138 @@ +assertEquals(<<<'PHP' +implements TransformerInterface { + /** + * {@inheritdoc} + */ + function transformToHttp($value, ElementInterface $input) + { + } + + /** + * {@inheritdoc} + */ + function transformFromHttp($value, ElementInterface $input) + { + } +} +PHP + , $generator->generateClass() +); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\ElementInterface; +use Bdf\Form\Transformer\TransformerInterface; + + +PHP + , $generator->printer()->printNamespace($generator->namespace()) +); + } + + /** + * @return void + */ + public function test_withPromotedProperty() + { + $generator = new TransformerClassGenerator(new PhpNamespace('Generated')); + $generator->withPromotedProperty('foo')->setPrivate()->setType(FormInterface::class); + + $this->assertEquals(<<<'PHP' +implements TransformerInterface { + /** + * {@inheritdoc} + */ + function transformToHttp($value, ElementInterface $input) + { + } + + /** + * {@inheritdoc} + */ + function transformFromHttp($value, ElementInterface $input) + { + } + + public function __construct(private \Bdf\Form\Aggregate\FormInterface $foo) + { + } +} +PHP + , $generator->generateClass() +); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\ElementInterface; +use Bdf\Form\Transformer\TransformerInterface; + + +PHP + , $generator->printer()->printNamespace($generator->namespace()) +); + } + + /** + * @return void + */ + public function test_with_methods_body() + { + $generator = new TransformerClassGenerator(new PhpNamespace('Generated')); + + $generator->toHttp()->setBody('return $value + 2;'); + $generator->fromHttp()->setBody('return $value - 2;'); + + $this->assertEquals(<<<'PHP' +implements TransformerInterface { + /** + * {@inheritdoc} + */ + function transformToHttp($value, ElementInterface $input) + { + return $value + 2; + } + + /** + * {@inheritdoc} + */ + function transformFromHttp($value, ElementInterface $input) + { + return $value - 2; + } +} +PHP + , $generator->generateClass() + ); + + $this->assertEquals(<<<'PHP' +namespace Generated; + +use Bdf\Form\ElementInterface; +use Bdf\Form\Transformer\TransformerInterface; + + +PHP + , $generator->printer()->printNamespace($generator->namespace()) + ); + } +} diff --git a/tests/Processor/CompileAttributesProcessorTest.php b/tests/Processor/CompileAttributesProcessorTest.php index 6898ac2..d6fc2f2 100644 --- a/tests/Processor/CompileAttributesProcessorTest.php +++ b/tests/Processor/CompileAttributesProcessorTest.php @@ -5,7 +5,7 @@ use Bdf\Form\Aggregate\FormBuilder; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Child\GetSet; -use Bdf\Form\Attribute\Constraint\CustomConstraint; +use Bdf\Form\Attribute\Constraint\CallbackConstraint; use Bdf\Form\Attribute\Form\Generates; use Bdf\Form\Attribute\Processor\CompileAttributesProcessor; use Bdf\Form\Leaf\IntegerElement; @@ -224,15 +224,98 @@ public function test_file_already_exists_but_without_class_on_file() $this->assertStringEqualsFile($file, $code); } + + public function test_generate() + { + $filename = '/tmp/manual_generated_configurator.php'; + + file_put_contents($filename, 'invalid php file'); + + $processor = new CompileAttributesProcessor( + fn (AttributeForm $form) => 'Generated\ManualConfigurator', + fn (string $className) => $filename + ); + + $form = new MyForm(); + + $processor->generate($form); + + $this->assertFileExists($filename); + $this->assertStringEqualsFile( + $filename, + <<<'PHP' +generates(Person::class); + + $firstName = $builder->add('firstName', StringElement::class); + $firstName->satisfy(new ClosureConstraint([$form, 'validateName'])); + $firstName->hydrator(new Setter(null))->extractor(new Getter(null)); + $firstName->satisfy(new NotBlank()); + + $lastName = $builder->add('lastName', StringElement::class); + $lastName->satisfy(new ClosureConstraint([$form, 'validateName'])); + $lastName->hydrator(new Setter(null))->extractor(new Getter(null)); + + $age = $builder->add('age', IntegerElement::class); + $age->hydrator(new Setter(null))->extractor(new Getter(null)); + $age->satisfy(new Positive()); + $age->satisfy(new LessThan(150)); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + (\Closure::bind(function () use ($inner, $form) { + $form->firstName = $inner['firstName']->element(); + $form->lastName = $inner['lastName']->element(); + $form->age = $inner['age']->element(); + }, null, MyForm::class))(); + } +} + +PHP + ); + + $this->assertFalse(class_exists('Generated\ManualConfigurator', false)); + } } #[Generates(Person::class)] class MyForm extends AttributeForm { - #[NotBlank, CustomConstraint('validateName'), GetSet] + #[NotBlank, CallbackConstraint('validateName'), GetSet] private StringElement $firstName; - #[CustomConstraint('validateName'), GetSet] + #[CallbackConstraint('validateName'), GetSet] private StringElement $lastName; #[Positive, LessThan(150), GetSet] diff --git a/tests/Processor/ConfigureFormBuilderStrategyTest.php b/tests/Processor/ConfigureFormBuilderStrategyTest.php deleted file mode 100644 index 178c72e..0000000 --- a/tests/Processor/ConfigureFormBuilderStrategyTest.php +++ /dev/null @@ -1,46 +0,0 @@ -registerElementAttributeProcessor(new class implements ElementAttributeProcessorInterface { - public function type(): string { return Foo::class; } - - public function process(ChildBuilderInterface $builder, object $attribute): void - { - $builder->default('Foo'); - } - - public function generateCode(string $name, AttributesProcessorGenerator $generator, \ReflectionAttribute $attribute): void - { - - } - }); - - $form = new class(null, new ReflectionProcessor($strategy)) extends AttributeForm { - #[Foo] - public StringElement $foo; - }; - - $form->submit([]); - - $this->assertEquals('Foo', $form->foo->value()); - } -} - -#[\Attribute] -class Foo {} diff --git a/tests/Processor/Element/TransformerAttributeProcessorTest.php b/tests/Processor/Element/TransformerAttributeProcessorTest.php index 7ebdb57..ad09f11 100644 --- a/tests/Processor/Element/TransformerAttributeProcessorTest.php +++ b/tests/Processor/Element/TransformerAttributeProcessorTest.php @@ -79,7 +79,7 @@ class ATransformer implements TransformerInterface { public function transformToHttp($value, ElementInterface $input) { - // TODO: Implement transformToHttp() method. + } public function transformFromHttp($value, ElementInterface $input) @@ -93,7 +93,7 @@ class BTransformer implements TransformerInterface { public function transformToHttp($value, ElementInterface $input) { - // TODO: Implement transformToHttp() method. + } public function transformFromHttp($value, ElementInterface $input) diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..28ebb3a --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ + Date: Wed, 8 Dec 2021 15:57:53 +0100 Subject: [PATCH 11/40] Add scrutinizer config + fix timezone on CI --- .github/workflows/php.yml | 12 +----------- .scrutinizer.yml | 20 ++++++++++++++++++++ tests/bootstrap.php | 1 + 3 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 .scrutinizer.yml diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 5fe0bc0..6ef0f38 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -16,12 +16,7 @@ jobs: steps: - uses: actions/checkout@v2 - - - name: Set Timezone - uses: szenius/set-timezone@v1.0 - with: - timezoneLinux: "Europe/Paris" - + - name: Install PHP uses: shivammathur/setup-php@v2 with: @@ -50,11 +45,6 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set Timezone - uses: szenius/set-timezone@v1.0 - with: - timezoneLinux: "Europe/Paris" - - name: Install PHP uses: shivammathur/setup-php@v2 with: diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..79d255c --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,20 @@ +build: + environment: + php: 8.0.2 + + nodes: + analysis: + tests: + override: + - php-scrutinizer-run + coverage: + tests: + override: + - command: composer run tests-with-coverage + coverage: + file: coverage.xml + format: clover + +filter: + excluded_paths: + - 'tests/*' diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 28ebb3a..f2485de 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,6 @@ Date: Wed, 8 Dec 2021 16:12:26 +0100 Subject: [PATCH 12/40] Fix scrutinizer build: add XDEBUG_MODE=coverage on command --- .scrutinizer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 79d255c..78bb2c0 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -10,7 +10,7 @@ build: coverage: tests: override: - - command: composer run tests-with-coverage + - command: XDEBUG_MODE=coverage composer run tests-with-coverage coverage: file: coverage.xml format: clover From 69eee08754aa4d7fba90f55a87de29a35e3ad97d Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 8 Dec 2021 16:34:24 +0100 Subject: [PATCH 13/40] Add README --- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5b8cc0 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# BDF Form attribute + +[![build](https://github.com/b2pweb/bdf-form-attribute/actions/workflows/php.yml/badge.svg)](https://github.com/b2pweb/bdf-form-attribute/actions/workflows/php.yml) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/b2pweb/bdf-form-attribute/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/b2pweb/bdf-form-attribute/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/b2pweb/bdf-form-attribute/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/b2pweb/bdf-form-attribute/?branch=master) +[![Packagist Version](https://img.shields.io/packagist/v/b2pweb/bdf-form-attribute.svg)](https://packagist.org/packages/b2pweb/bdf-form-attribute) +[![Total Downloads](https://img.shields.io/packagist/dt/b2pweb/bdf-form-attribute.svg)](https://packagist.org/packages/b2pweb/bdf-form-attribute) +[![Type Coverage](https://shepherd.dev/github/b2pweb/bdf-form-attribute/coverage.svg)](https://shepherd.dev/github/b2pweb/bdf-form-attribute) + +Declaring forms using PHP 8 attributes and typed properties, over [BDF form](https://github.com/b2pweb/bdf-form) + +## Usage + +### Install using composer + +``` +composer require b2pweb/bdf-form-attribute +``` + +### Declare a form class + +> Adaptation of example from BDF Form : [Handle entities](https://github.com/b2pweb/bdf-form#handle-entities) + +```php + +use Bdf\Form\Attribute\Form\Generates; +use Bdf\Form\Leaf\StringElement; +use Symfony\Component\Validator\Constraints\NotBlank; +use Bdf\Form\Attribute\Child\GetSet; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Leaf\Date\DateTimeElement; +use Bdf\Form\Attribute\Element\Date\ImmutableDateTime; +use Bdf\Form\Attribute\Child\CallbackModelTransformer; +use Bdf\Form\ElementInterface; + +// Declare the entity +class Person +{ + public string $firstName; + public string $lastName; + public ?DateTimeInterface $birthDate; + public ?Country $country; +} + +#[Generates(Person::class)] // Define that PersonForm::value() should return a Person instance +class PersonForm extends AttributeForm // The form must extend AttributeForm to use PHP 8 attributes syntax +{ + // Declare a property for declare an input on the form + // The property type is used as element type + // use NotBlank for mark the input as required + // GetSet will define entity accessor + #[NotBlank, GetSet] + private StringElement $firstName; + + #[NotBlank, GetSet] + private StringElement $lastName; + + // Use ImmutableDateTime to change the value of birthDate to DateTimeImmutable + #[ImmutableDateTime, GetSet] + private DateTimeElement $birthDate; + + // Custom transformer can be declared with a method name as first parameter of ModelTransformer + // Transformers methods must be declared as public on the form class + #[ImmutableDateTime, CallbackModelTransformer(toEntity: 'findCountry', toInput: 'extractCountryCode'), GetSet] + private StringElement $country; + + // Transformer used when extracting input value from entity + public function findCountry(Country $value, ElementInterface $element): string + { + return $value->code; + } + + // Transformer used when filling entity with input value + public function extractCountryCode(string $value, ElementInterface $element): ?Country + { + return Country::findByCode($value); + } +} +``` From 07472e00df84241671c26b69708088caa2b16c16 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 9 Dec 2021 10:46:10 +0100 Subject: [PATCH 14/40] Improve readme --- README.md | 95 +++++++++++++++++++++++++++++++++++++++++ src/Child/Configure.php | 1 - 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5b8cc0..80656f1 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,19 @@ composer require b2pweb/bdf-form-attribute ### Declare a form class +To create a form using PHP 8 attributes, first you have to extend [AttributeForm](src/AttributeForm.php). + +Then declare all input elements and buttons as property : +- For element : `public|protected|private MyElementType $myElementName;` +- For button : `public|protected|private ButtonInterface $myButton;` + +Finally, use attributes on properties (or form class) for configure elements, add constraints, transformers... + +```php +#[Positive, UnitTransformer, GetSet] +public IntegerElement $weight; +``` + > Adaptation of example from BDF Form : [Handle entities](https://github.com/b2pweb/bdf-form#handle-entities) ```php @@ -77,3 +90,85 @@ class PersonForm extends AttributeForm // The form must extend AttributeForm to } } ``` + +### Supported attributes + +This library supports various attributes types for configure form elements : + +- [Symfony validator](https://github.com/symfony/validator's) `Constraint`, translated as `...->satisfy(new Constraint(...))` +- [`ExtractorInterface`](https://github.com/b2pweb/bdf-form/blob/master/src/PropertyAccess/ExtractorInterface.php), translated as `...->extractor(new Extractor(...))` +- [`HydratorInterface`](https://github.com/b2pweb/bdf-form/blob/master/src/PropertyAccess/HydratorInterface.php), translated as `...->hydrator(new Hydrator(...))` +- [`FilterInterface`](https://github.com/b2pweb/bdf-form/blob/master/src/Filter/FilterInterface.php), translated as `...->filter(new Filter(...))` +- [`TransformerInterface`](https://github.com/b2pweb/bdf-form/blob/master/src/Transformer/TransformerInterface.php), translated as `...->transformer(new Transformer(...))` + +### Generate the configurator code from attributes + +To improve performance, and to do without the use of reflection, attributes can be used to generate the PHP code +of the configurator, instead of dynamically configure the form. + +To do that, use [`CompileAttributesProcessor`](src/Processor/CompileAttributesProcessor.php) as argument of form constructor. + +```php +const GENERATED_NAMESPACE = 'Generated\\'; +const GENERATED_DIRECTORY = __DIR__ . '/var/generated/form/'; + +// Configure the processor by setting class and file resolvers +$processor = new CompileAttributesProcessor( + fn (AttributeForm $form) => GENERATED_NAMESPACE . get_class($form) . 'Configurator', // Retrieve the configurator class name from the form object + fn (string $className) => GENERATED_DIRECTORY . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php', // Get the filename of the configurator class from the configurator class name +); + +$form = new MyForm(processor: $processor); // Set the processor on the constructor +$form->submit(['firstName' => 'John']); // Directly use the form : the configurator will be automatically generated + +// You can also pre-generate the form configurator using CompileAttributesProcessor::generate() +$processor->generate(new MyOtherForm()); +``` + +## Available attributes + +### On form class + +| Attribute | Example | Translated to | Purpose | +|-------------------------------------------------------|---------------------------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------| +| [`Generates`](src/Form/Generates.php) | `Generates(MyEntity::class)` | `$builder->generates(MyEntity::class)` | Define the entity class generated by the form. | +| [`CallbackGenerator`](src/Form/CallbackGenerator.php) | `CallbackGenerator('generate')` | `$builder->generates([$this, 'generate'])` | Define the method to use for generate the form value. The method must be declared as public on the form class. | +| [`Csrf`](src/Form/Csrf.php) | `Csrf(tokenId: 'MyToken')` | `$builder->csrf()->tokenId('MyToken')` | Add a CSRF element on the form. | + +### On button property + +| Attribute | Example | Translated to | Purpose | +|-----------------------------------|------------------------|-------------------------------|-------------------------------------------------------------------| +| [`Groups`](src/Button/Groups.php) | `Groups('foo', 'bar')` | `...->groups(['foo', 'bar'])` | Define validation groups to use when the given button is clicked. | +| [`Value`](src/Button/Value.php) | `Value('foo')` | `...->value('foo')` | Define the button value. | + +### On element property + +| Attribute | Example | Translated to | Purpose | +|----------------------------------------------------------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Child** | | | | +| [`ModelTransformer`](src/Child/ModelTransformer.php) | `ModelTransformer(MyTransformer::class, ['ctroarg'])` | `...->modelTransformer(new MyTransformer('ctorarg))` | Define a [model transformer](https://github.com/b2pweb/bdf-form#10---apply-model-transformer-scope-child) on the current child. | +| [`CallbackModelTransformer`](src/Child/CallbackModelTransformer.php) | `CallbackModelTransformer(toEntity: 'parseInput', toInput: 'normalize')` | `...->modelTransformer(fn ($value, $input, $toEntity) => $toEntity ? $this->parseInput($value, $input) : $this->normalize($value, $input))` | Define a [model transformer](https://github.com/b2pweb/bdf-form#10---apply-model-transformer-scope-child) using a form method. | +| [`Configure`](src/Child/Configure.php) | `Configure('configureInput')` | `...->configureField($elementBuilder)` | Manually configure the element builder using a form method. The method must be public and declared on the form class. | +| [`DefaultValue`](src/Child/DefaultValue.php) | `DefaultValue(42)` | `...->configureField($elementBuilder)` | Define the default value of the input. | +| [`Dependencies`](src/Child/Dependencies.php) | `Dependencies('foo', 'bar')` | `...->depends('foo', 'bar')` | Declare dependencies on the current input. Dependencies will be submitted before the current field. | +| [`GetSet`](src/Child/GetSet.php) | `GetSet('realField')` | `...->getter('realField')->setter('realField')` | Enable hydration and extraction of the entity. | +| **Element** | | | | +| [`CallbackConstraint`](src/Constraint/CallbackConstraint.php) | `CallbackConstraint('validateInput')` | `...->satisfy([$this, 'validateInput'])` | Validate an input using a method. | +| [`Satisfy`](src/Constraint/Satisfy.php) | `Satisfy(MyConstraint::class, ['opt' => 'val'])` | `...->satisfy(new MyConstraint(['opt' => 'val']))` | Add a constraint on the input. Prefer directly use the constraint class as attribute if possible. | +| [`Transformer`](src/Element/Transformer.php) | `Transformer(MyTransformer::class, ['ctorarg'])` | `...->transformer(new MyTransformer('ctorarg))` | Add a [transformer](https://github.com/b2pweb/bdf-form#6---call-element-transformers-scope-element) on the input. Prefer directly use the transformer class as attribute if possible. | +| [`CallbackTransformer`](src/Element/CallbackTransformer.php) | `CallbackTransformer(fromHttp: 'parse', toHttp: 'stringify')` | `...->transformer(fn ($value, $input, $toPhp) => $toPhp ? $this->parse($value, $input) : $this->stringify($value, $input))` | Add a [transformer](https://github.com/b2pweb/bdf-form#6---call-element-transformers-scope-element) using a form method. | +| [`Choices`](src/Element/Choices.php) | `Choices(['foo', 'bar'])` | `...->choices(['foo', 'bar'])` | Define the values choices of the input. Supports using a method as choices provider. | +| [`Raw`](src/Element/Raw.php) | `Raw` | `...->raw()` | For number elements. Use native PHP cast instead of locale parsing for convert number. | +| [`TransformerError`](src/Element/TransformerError.php) | `TransformerError(message: 'Invalid value provided')` | `...->transformerErrorMessage('Invalid value provided')` | Configure error handling of transformer exceptions. | +| [`IgnoreTransformerException`](src/Element/IgnoreTransformerException.php) | `IgnoreTransformerException` | `...->ignoreTransformerException()` | Ignore transformer exception. If enable and an exception occurs, the raw value will be used. | +| **DateTimeElement** | | | | +| [`DateFormat`](src/Element/Date/DateFormat.php) | `DateFormat('d/m/Y H:i')` | `...->format('d/m/Y H:i')` | Define the input date format. | +| [`DateTimeClass`](src/Element/Date/DateTimeClass.php) | `DateTimeClass(Carbon::class)` | `...->className(Carbon::class)` | Define date time class to use on for parse the date. | +| [`ImmutableDateTime`](src/Element/Date/ImmutableDateTime.php) | `ImmutableDateTime` | `...->immutable()` | Use `DateTimeImmutable` as date time class. | +| [`Timezone`](src/Element/Date/Timezone.php) | `Timezone('Europe/Paris')` | `...->timezone('Europe/Paris')` | Define the parsing and normalized timezone to use. | +| **ArrayElement** | | | | +| [`ArrayConstraint`](src/Aggregate/ArrayConstraint.php) | `ArrayConstraint(MyConstraint::class, ['opt' => 'val'])` | `...->arrayConstraint(new MyConstraint(['opt' => 'val']))` | Add a constraint on the whole array element. | +| [`CallbackArrayConstraint`](src/Aggregate/CallbackArrayConstraint.php) | `CallbackArrayConstraint('validateInput')` | `...->arrayConstraint([$this, 'validateInput'])` | Add a constraint on the whole array element, using a form method. | +| [`Count`](src/Aggregate/Count.php) | `Count(min: 3, max: 6)` | `...->arrayConstraint(new Count(min: 3, max: 6))` | Add a Count constraint on the array element. | +| [`ElementType`](src/Aggregate/ElementType.php) | `ElementType(IntegerElement::class, 'configureElement')` | `...->element(IntegerElement::class, [$this, 'configureElement'])` | Define the array element type. A configuration callback method can be define for configure the inner element. | diff --git a/src/Child/Configure.php b/src/Child/Configure.php index c95370b..0f4ac69 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -29,7 +29,6 @@ * } * */ -// @todo date time after et before attributes #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] class Configure implements ChildBuilderAttributeInterface { From 8f964d5a42aeed27c8a9561d5bb143b8d39f4eef Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 9 Dec 2021 10:50:25 +0100 Subject: [PATCH 15/40] Fix symfony validator link on readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80656f1..77b47ff 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ class PersonForm extends AttributeForm // The form must extend AttributeForm to This library supports various attributes types for configure form elements : -- [Symfony validator](https://github.com/symfony/validator's) `Constraint`, translated as `...->satisfy(new Constraint(...))` +- [Symfony validator's](https://github.com/symfony/validator) `Constraint`, translated as `...->satisfy(new Constraint(...))` - [`ExtractorInterface`](https://github.com/b2pweb/bdf-form/blob/master/src/PropertyAccess/ExtractorInterface.php), translated as `...->extractor(new Extractor(...))` - [`HydratorInterface`](https://github.com/b2pweb/bdf-form/blob/master/src/PropertyAccess/HydratorInterface.php), translated as `...->hydrator(new Hydrator(...))` - [`FilterInterface`](https://github.com/b2pweb/bdf-form/blob/master/src/Filter/FilterInterface.php), translated as `...->filter(new Filter(...))` From 47ac3e8bae869b5c73ff9bcf2a515fb7568507d9 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 9 Dec 2021 11:42:52 +0100 Subject: [PATCH 16/40] Set attributes properties visibility to private --- src/Aggregate/ArrayConstraint.php | 6 ++++-- src/Aggregate/CallbackArrayConstraint.php | 6 ++++-- src/Aggregate/ElementType.php | 6 ++++-- src/AttributeForm.php | 1 + src/Button/Groups.php | 3 ++- src/Button/Value.php | 3 ++- src/Child/CallbackModelTransformer.php | 22 +++++++++++++--------- src/Child/Configure.php | 3 ++- src/Child/DefaultValue.php | 5 ++++- src/Child/Dependencies.php | 3 ++- src/Child/GetSet.php | 4 +++- src/Child/ModelTransformer.php | 6 ++++-- src/Constraint/CallbackConstraint.php | 6 ++++-- src/Constraint/Satisfy.php | 6 ++++-- src/Element/CallbackTransformer.php | 22 +++++++++++++--------- src/Element/Choices.php | 9 ++++++--- src/Element/Date/DateFormat.php | 4 +++- src/Element/Date/DateTimeClass.php | 3 ++- src/Element/Date/Timezone.php | 3 ++- src/Element/IgnoreTransformerException.php | 4 +++- src/Element/Raw.php | 4 +++- src/Element/Transformer.php | 6 ++++-- src/Element/TransformerError.php | 11 ++++++++--- src/Form/CallbackGenerator.php | 4 +++- src/Form/Csrf.php | 12 ++++++++---- src/Form/Generates.php | 3 ++- 26 files changed, 110 insertions(+), 55 deletions(-) diff --git a/src/Aggregate/ArrayConstraint.php b/src/Aggregate/ArrayConstraint.php index 24d0ed4..b14b79f 100644 --- a/src/Aggregate/ArrayConstraint.php +++ b/src/Aggregate/ArrayConstraint.php @@ -45,14 +45,16 @@ public function __construct( * The constraint class name * * @var class-string + * @readonly */ - public string $constraint, + private string $constraint, /** * Constraint's constructor options * * @var mixed|null + * @readonly */ - public mixed $options = null + private mixed $options = null ) { } diff --git a/src/Aggregate/CallbackArrayConstraint.php b/src/Aggregate/CallbackArrayConstraint.php index 7e9de3f..1738428 100644 --- a/src/Aggregate/CallbackArrayConstraint.php +++ b/src/Aggregate/CallbackArrayConstraint.php @@ -60,16 +60,18 @@ public function __construct( * - Return array with error message and code * * @var literal-string + * @readonly */ - public string $methodName, + private string $methodName, /** * The error message to use * This option is used only if the validator return false, in other cases, * the message returned by the validator will be used * * @var string|null + * @readonly */ - public ?string $message = null, + private ?string $message = null, ) { } diff --git a/src/Aggregate/ElementType.php b/src/Aggregate/ElementType.php index 50be39b..e9d07cb 100644 --- a/src/Aggregate/ElementType.php +++ b/src/Aggregate/ElementType.php @@ -50,15 +50,17 @@ public function __construct( * The form element class name * * @var class-string + * @readonly */ - public string $elementType, + private string $elementType, /** * The element configuration method name * This method must be defined on the form class, and with public visibility * * @var literal-string|null + * @readonly */ - public ?string $configurator = null + private ?string $configurator = null ) { } diff --git a/src/AttributeForm.php b/src/AttributeForm.php index 395e2c0..f31fb80 100644 --- a/src/AttributeForm.php +++ b/src/AttributeForm.php @@ -20,6 +20,7 @@ abstract class AttributeForm extends CustomForm * and for configure the form builder * * @var AttributesProcessorInterface + * @readonly */ private AttributesProcessorInterface $processor; diff --git a/src/Button/Groups.php b/src/Button/Groups.php index 9ee37cd..d0a0597 100644 --- a/src/Button/Groups.php +++ b/src/Button/Groups.php @@ -38,8 +38,9 @@ final class Groups implements ButtonBuilderAttributeInterface { /** * @var list + * @readonly */ - public array $groups; + private array $groups; /** * @param string ...$groups List of validation groups diff --git a/src/Button/Value.php b/src/Button/Value.php index 62a88bb..4f713bd 100644 --- a/src/Button/Value.php +++ b/src/Button/Value.php @@ -35,8 +35,9 @@ final class Value implements ButtonBuilderAttributeInterface public function __construct( /** * The button HTTP value + * @readonly */ - public string $value, + private string $value, ) { } diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index 6497b0b..08f008f 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -78,20 +78,23 @@ public function __construct( * If defined, the other parameters will be ignored * * @var literal-string|null + * @readonly */ - public ?string $callback = null, + private ?string $callback = null, /** * Method name use to define the transformation process from input value to the entity * * @var literal-string|null + * @readonly */ - public ?string $toEntity = null, + private ?string $toEntity = null, /** * Method name use to define the transformation process from entity value to input * * @var literal-string|null + * @readonly */ - public ?string $toInput = null, + private ?string $toInput = null, ) { } @@ -105,10 +108,11 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ return; } - $builder->modelTransformer(new class ($form, $this) implements TransformerInterface { + $builder->modelTransformer(new class ($form, $this->toInput, $this->toEntity) implements TransformerInterface { public function __construct( private AttributeForm $form, - private CallbackModelTransformer $attribute, + private ?string $toInput, + private ?string $toEntity, ) { } @@ -117,11 +121,11 @@ public function __construct( */ public function transformToHttp($value, ElementInterface $input) { - if (!$this->attribute->toInput) { + if (!$this->toInput) { return $value; } - return $this->form->{$this->attribute->toInput}($value, $input); + return $this->form->{$this->toInput}($value, $input); } /** @@ -129,11 +133,11 @@ public function transformToHttp($value, ElementInterface $input) */ public function transformFromHttp($value, ElementInterface $input) { - if (!$this->attribute->toEntity) { + if (!$this->toEntity) { return $value; } - return $this->form->{$this->attribute->toEntity}($value, $input); + return $this->form->{$this->toEntity}($value, $input); } }); } diff --git a/src/Child/Configure.php b/src/Child/Configure.php index 0f4ac69..7ed2fdd 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -38,8 +38,9 @@ public function __construct( * The method should follow the prototype `public function (ChildBuilderInterface $builder): void` * * @var literal-string + * @readonly */ - public string $callback + private string $callback ) { } diff --git a/src/Child/DefaultValue.php b/src/Child/DefaultValue.php index 8bd1670..6e5aec3 100644 --- a/src/Child/DefaultValue.php +++ b/src/Child/DefaultValue.php @@ -35,7 +35,10 @@ final class DefaultValue implements ChildBuilderAttributeInterface { public function __construct( - public mixed $default + /** + * @readonly + */ + private mixed $default ) { } diff --git a/src/Child/Dependencies.php b/src/Child/Dependencies.php index 1390993..2948d27 100644 --- a/src/Child/Dependencies.php +++ b/src/Child/Dependencies.php @@ -35,8 +35,9 @@ final class Dependencies implements ChildBuilderAttributeInterface { /** * @var list + * @readonly */ - public array $dependencies; + private array $dependencies; /** * @param string ...$dependencies List of inputs names diff --git a/src/Child/GetSet.php b/src/Child/GetSet.php index 5c99ce7..2738c94 100644 --- a/src/Child/GetSet.php +++ b/src/Child/GetSet.php @@ -47,8 +47,10 @@ public function __construct( * (optionally starting with get for the getter, and starting with set for the setter) * * If not provided, the input name will be used as property name + * + * @readonly */ - public ?string $propertyName = null, + private ?string $propertyName = null, ) { } diff --git a/src/Child/ModelTransformer.php b/src/Child/ModelTransformer.php index d1e8531..c38c478 100644 --- a/src/Child/ModelTransformer.php +++ b/src/Child/ModelTransformer.php @@ -39,14 +39,16 @@ public function __construct( * The transformer class name * * @var class-string + * @readonly */ - public string $transformerClass, + private string $transformerClass, /** * Arguments to provide on the transformer constructor * * @var array + * @readonly */ - public array $constructorArguments = [], + private array $constructorArguments = [], ) { } diff --git a/src/Constraint/CallbackConstraint.php b/src/Constraint/CallbackConstraint.php index d6108a0..a79e7ce 100644 --- a/src/Constraint/CallbackConstraint.php +++ b/src/Constraint/CallbackConstraint.php @@ -58,16 +58,18 @@ public function __construct( * - Return array with error message and code * * @var literal-string + * @readonly */ - public string $methodName, + private string $methodName, /** * The error message to use * This option is used only if the validator return false, in other cases, * the message returned by the validator will be used * * @var string|null + * @readonly */ - public ?string $message = null, + private ?string $message = null, ) { } diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index 8223916..1c0d706 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -41,14 +41,16 @@ public function __construct( * The constraint class name * * @var class-string + * @readonly */ - public string $constraint, + private string $constraint, /** * Constraint's constructor options * * @var array|null|string + * @readonly */ - public mixed $options = null + private mixed $options = null ) { } diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index cd1f99a..f28e15b 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -81,20 +81,23 @@ public function __construct( * If defined, the other parameters will be ignored * * @var literal-string|null + * @readonly */ - public ?string $callback = null, + private ?string $callback = null, /** * Method name use to define the transformation process from http value to the input * * @var literal-string|null + * @readonly */ - public ?string $fromHttp = null, + private ?string $fromHttp = null, /** * Method name use to define the transformation process from input value to http format * * @var literal-string|null + * @readonly */ - public ?string $toHttp = null, + private ?string $toHttp = null, ) { } @@ -108,10 +111,11 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ return; } - $builder->transformer(new class ($form, $this) implements TransformerInterface { + $builder->transformer(new class ($form, $this->fromHttp, $this->toHttp) implements TransformerInterface { public function __construct( private AttributeForm $form, - private CallbackTransformer $attribute, + private ?string $fromHttp, + private ?string $toHttp, ) { } @@ -120,11 +124,11 @@ public function __construct( */ public function transformToHttp($value, ElementInterface $input) { - if (!$this->attribute->toHttp) { + if (!$this->toHttp) { return $value; } - return $this->form->{$this->attribute->toHttp}($value, $input); + return $this->form->{$this->toHttp}($value, $input); } /** @@ -132,11 +136,11 @@ public function transformToHttp($value, ElementInterface $input) */ public function transformFromHttp($value, ElementInterface $input) { - if (!$this->attribute->fromHttp) { + if (!$this->fromHttp) { return $value; } - return $this->form->{$this->attribute->fromHttp}($value, $input); + return $this->form->{$this->fromHttp}($value, $input); } }); } diff --git a/src/Element/Choices.php b/src/Element/Choices.php index e9d5153..654f483 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -79,21 +79,24 @@ public function __construct( * The label is not required. * * @var literal-string|array + * @readonly */ - public string|array $choices, + private string|array $choices, /** * The error message * If not provided, a default message will be used * * @var string|null + * @readonly */ - public ?string $message = null, + private ?string $message = null, /** * Extra constraint options * * @var array + * @readonly */ - public array $options = [], + private array $options = [], ) { } diff --git a/src/Element/Date/DateFormat.php b/src/Element/Date/DateFormat.php index 375e3d8..124190e 100644 --- a/src/Element/Date/DateFormat.php +++ b/src/Element/Date/DateFormat.php @@ -39,9 +39,11 @@ public function __construct( * The date format * * @var non-empty-string + * @readonly + * * @see https://www.php.net/manual/en/datetime.createfromformat.php#refsect1-datetime.createfromformat-parameters For the format */ - public string $format, + private string $format, ) { } diff --git a/src/Element/Date/DateTimeClass.php b/src/Element/Date/DateTimeClass.php index f5615af..54f878d 100644 --- a/src/Element/Date/DateTimeClass.php +++ b/src/Element/Date/DateTimeClass.php @@ -40,8 +40,9 @@ public function __construct( * The datetime class to use * * @var class-string<\DateTimeInterface> + * @readonly */ - public string $className, + private string $className, ) { } diff --git a/src/Element/Date/Timezone.php b/src/Element/Date/Timezone.php index 45a44ed..8791775 100644 --- a/src/Element/Date/Timezone.php +++ b/src/Element/Date/Timezone.php @@ -39,8 +39,9 @@ public function __construct( * The timezone name or offset * * @var non-empty-string + * @readonly */ - public string $timezone, + private string $timezone, ) { } diff --git a/src/Element/IgnoreTransformerException.php b/src/Element/IgnoreTransformerException.php index 25be0ef..1cbe1ad 100644 --- a/src/Element/IgnoreTransformerException.php +++ b/src/Element/IgnoreTransformerException.php @@ -43,8 +43,10 @@ final class IgnoreTransformerException implements ChildBuilderAttributeInterface public function __construct( /** * Ignore or not transformer errors + * + * @readonly */ - public bool $ignore = true, + private bool $ignore = true, ) { } diff --git a/src/Element/Raw.php b/src/Element/Raw.php index 7fc5139..30c41cc 100644 --- a/src/Element/Raw.php +++ b/src/Element/Raw.php @@ -38,8 +38,10 @@ final class Raw implements ChildBuilderAttributeInterface public function __construct( /** * Enable or disable raw mode for parsing numbers + * + * @readonly */ - public bool $flag = true, + private bool $flag = true, ) { } diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index e6cb3b5..9602426 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -42,14 +42,16 @@ public function __construct( * The transformer class name * * @var class-string + * @readonly */ - public string $transformerClass, + private string $transformerClass, /** * Arguments to provide on the transformer constructor * * @var array + * @readonly */ - public array $constructorArguments = [], + private array $constructorArguments = [], ) { } diff --git a/src/Element/TransformerError.php b/src/Element/TransformerError.php index 6eecb2d..e9c55d3 100644 --- a/src/Element/TransformerError.php +++ b/src/Element/TransformerError.php @@ -44,12 +44,16 @@ class TransformerError implements ChildBuilderAttributeInterface public function __construct( /** * The error message to show when transformer fail + * + * @readonly */ - public ?string $message = null, + private ?string $message = null, /** * The error code to provide when transformer fail + * + * @readonly */ - public ?string $code = null, + private ?string $code = null, /** * Method name to use for validate the transformer exception * @@ -60,8 +64,9 @@ public function __construct( * Else, the method should fill `TransformerExceptionConstraint` with error message and code to provide the custom error * * @var literal-string|null + * @readonly */ - public ?string $validationCallback = null, + private ?string $validationCallback = null, ) { } diff --git a/src/Form/CallbackGenerator.php b/src/Form/CallbackGenerator.php index 0dd4842..ede5a92 100644 --- a/src/Form/CallbackGenerator.php +++ b/src/Form/CallbackGenerator.php @@ -44,8 +44,10 @@ public function __construct( * The method name use for generate the form value * This method should be public, and declared on the form class, following the prototype : * `public function (FormInterface $form): mixed` + * + * @readonly */ - public string $callback, + private string $callback, ) { } diff --git a/src/Form/Csrf.php b/src/Form/Csrf.php index 1b65b79..f028adb 100644 --- a/src/Form/Csrf.php +++ b/src/Form/Csrf.php @@ -40,34 +40,38 @@ public function __construct( * The token input name * * @var non-empty-string + * @readonly */ - public string $name = '_token', + private string $name = '_token', /** * The token id * By default is value is the class name of `CsrfElement` * * @var string|null + * @readonly * * @see CsrfTokenManagerInterface::getToken() The parameter tokenId will be used as parameter of this method * @see CsrfElementBuilder::tokenId() The called method if defined */ - public ?string $tokenId = null, + private ?string $tokenId = null, /** * The error message to display if the token do not correspond * * @var string|null + * @readonly * * @see CsrfElementBuilder::message() The called method if defined */ - public ?string $message = null, + private ?string $message = null, /** * Always invalidate the CSRF token after submission * * @var bool|null + * @readonly * * @see CsrfElementBuilder::invalidate() The called method if defined */ - public ?bool $invalidate = null, + private ?bool $invalidate = null, ) { } diff --git a/src/Form/Generates.php b/src/Form/Generates.php index 3573beb..95ed8c1 100644 --- a/src/Form/Generates.php +++ b/src/Form/Generates.php @@ -41,8 +41,9 @@ public function __construct( * The entity class name to generate * * @var class-string + * @readonly */ - public string $className, + private string $className, ) { } From af7c0e5ecca72753f382b44036afed01ccd50c92 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 9 Dec 2021 12:13:00 +0100 Subject: [PATCH 17/40] Fix builder calls order shoud corresponds to attributes order --- src/Processor/ConfigureFormBuilderStrategy.php | 18 +++++++++++------- src/Processor/GenerateConfiguratorStrategy.php | 15 +++++++++------ tests/Child/CallbackModelTransformerTest.php | 4 ++-- tests/Child/ModelTransformerTest.php | 4 ++-- tests/FunctionalTest.php | 6 +++--- .../CompileAttributesProcessorTest.php | 8 ++++---- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/Processor/ConfigureFormBuilderStrategy.php b/src/Processor/ConfigureFormBuilderStrategy.php index 189bbd3..cd081fa 100644 --- a/src/Processor/ConfigureFormBuilderStrategy.php +++ b/src/Processor/ConfigureFormBuilderStrategy.php @@ -65,14 +65,18 @@ public function onElementProperty(ReflectionProperty $property, string $name, st { $elementBuilder = $builder->add($name, $elementType); - foreach ($property->getAttributes(ChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $attribute->newInstance()->applyOnChildBuilder($form, $elementBuilder); - } + foreach ($property->getAttributes() as $attribute) { + $attributeInstance = $attribute->newInstance(); + + if ($attributeInstance instanceof ChildBuilderAttributeInterface) { + /** @var ChildBuilderAttributeInterface $attributeInstance */ + $attributeInstance->applyOnChildBuilder($form, $elementBuilder); + continue; + } - foreach ($this->elementProcessors as $configurator) { - foreach ($property->getAttributes($configurator->type(), ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { - $configurator->process($elementBuilder, $attribute->newInstance()); + foreach ($this->elementProcessors as $configurator) { + if ($attributeInstance instanceof ($configurator->type())) { + $configurator->process($elementBuilder, $attributeInstance); } } } diff --git a/src/Processor/GenerateConfiguratorStrategy.php b/src/Processor/GenerateConfiguratorStrategy.php index d9f86d4..5e2ae5f 100644 --- a/src/Processor/GenerateConfiguratorStrategy.php +++ b/src/Processor/GenerateConfiguratorStrategy.php @@ -81,13 +81,16 @@ public function onElementProperty(ReflectionProperty $property, string $name, st $elementType = $this->generator->useAndSimplifyType($elementType); $this->generator->line('$? = $builder->add(?, ?::class);', [$name, $name, new Literal($elementType)]); - foreach ($property->getAttributes(ChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $attribute->newInstance()->generateCodeForChildBuilder($name, $this->generator, $form); - } + 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) { - foreach ($property->getAttributes($configurator->type(), ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - if (!is_subclass_of($attribute->getName(), ChildBuilderAttributeInterface::class)) { + foreach ($this->elementProcessors as $configurator) { + if (is_subclass_of($attribute->getName(), $configurator->type())) { $configurator->generateCode($name, $this->generator, $attribute); } } diff --git a/tests/Child/CallbackModelTransformerTest.php b/tests/Child/CallbackModelTransformerTest.php index cb0cc42..91e5fda 100644 --- a/tests/Child/CallbackModelTransformerTest.php +++ b/tests/Child/CallbackModelTransformerTest.php @@ -137,8 +137,8 @@ public function __construct(private $form) { } }); - $foo->hydrator(new Setter()); $foo->extractor(new Getter()); + $foo->hydrator(new Setter()); $bar = $builder->add('bar', IntegerElement::class); $bar->modelTransformer(new class ($form) implements TransformerInterface { @@ -162,8 +162,8 @@ public function __construct(private $form) { } }); - $bar->hydrator(new Setter()); $bar->extractor(new Getter()); + $bar->hydrator(new Setter()); return $this; } diff --git a/tests/Child/ModelTransformerTest.php b/tests/Child/ModelTransformerTest.php index 07db1da..a876cfe 100644 --- a/tests/Child/ModelTransformerTest.php +++ b/tests/Child/ModelTransformerTest.php @@ -86,13 +86,13 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $a = $builder->add('a', StringElement::class); $a->modelTransformer(new ATransformer()); - $a->hydrator(new Setter()); $a->extractor(new Getter()); + $a->hydrator(new Setter()); $b = $builder->add('b', IntegerElement::class); $b->modelTransformer(new BTransformer()); - $b->hydrator(new Setter()); $b->extractor(new Getter()); + $b->hydrator(new Setter()); $c = $builder->add('c', StringElement::class); $c->modelTransformer(new TransformerWithArguments('foo_')); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index c8c8d99..987e262 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -146,15 +146,15 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? { $bar = $builder->add('bar', IntegerElement::class); $bar->satisfy(new NotBlank()); - $bar->satisfy(new GreaterThan(5)); - $bar->hydrator(new Setter()); $bar->extractor(new Getter()); + $bar->hydrator(new Setter()); + $bar->satisfy(new GreaterThan(5)); $foo = $builder->add('foo', StringElement::class); $foo->satisfy(new NotBlank()); - $foo->hydrator(new Setter()); $foo->extractor(new Getter()); + $foo->hydrator(new Setter()); return $this; } diff --git a/tests/Processor/CompileAttributesProcessorTest.php b/tests/Processor/CompileAttributesProcessorTest.php index d6fc2f2..9df1e3f 100644 --- a/tests/Processor/CompileAttributesProcessorTest.php +++ b/tests/Processor/CompileAttributesProcessorTest.php @@ -72,18 +72,18 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $builder->generates(Person::class); $firstName = $builder->add('firstName', StringElement::class); + $firstName->satisfy(new NotBlank()); $firstName->satisfy(new ClosureConstraint([$form, 'validateName'])); $firstName->hydrator(new Setter(null))->extractor(new Getter(null)); - $firstName->satisfy(new NotBlank()); $lastName = $builder->add('lastName', StringElement::class); $lastName->satisfy(new ClosureConstraint([$form, 'validateName'])); $lastName->hydrator(new Setter(null))->extractor(new Getter(null)); $age = $builder->add('age', IntegerElement::class); - $age->hydrator(new Setter(null))->extractor(new Getter(null)); $age->satisfy(new Positive()); $age->satisfy(new LessThan(150)); + $age->hydrator(new Setter(null))->extractor(new Getter(null)); return $this; } @@ -273,18 +273,18 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $builder->generates(Person::class); $firstName = $builder->add('firstName', StringElement::class); + $firstName->satisfy(new NotBlank()); $firstName->satisfy(new ClosureConstraint([$form, 'validateName'])); $firstName->hydrator(new Setter(null))->extractor(new Getter(null)); - $firstName->satisfy(new NotBlank()); $lastName = $builder->add('lastName', StringElement::class); $lastName->satisfy(new ClosureConstraint([$form, 'validateName'])); $lastName->hydrator(new Setter(null))->extractor(new Getter(null)); $age = $builder->add('age', IntegerElement::class); - $age->hydrator(new Setter(null))->extractor(new Getter(null)); $age->satisfy(new Positive()); $age->satisfy(new LessThan(150)); + $age->hydrator(new Setter(null))->extractor(new Getter(null)); return $this; } From f2938b450b9599211bf2f5991b363291830363e7 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 9 Dec 2021 12:42:09 +0100 Subject: [PATCH 18/40] Use LazyChoice instead of LazzyChoice --- src/Element/Choices.php | 10 +++++----- tests/Element/ChoicesTest.php | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Element/Choices.php b/src/Element/Choices.php index 654f483..5a8aad0 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -13,7 +13,7 @@ use Bdf\Form\Choice\Choiceable; use Bdf\Form\Choice\ChoiceBuilderTrait; use Bdf\Form\Choice\ChoiceInterface; -use Bdf\Form\Choice\LazzyChoice; +use Bdf\Form\Choice\LazyChoice; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Leaf\StringElementBuilder; use Nette\PhpGenerator\Literal; @@ -63,7 +63,7 @@ * @see ChoiceBuilderTrait::choices() The called method * @see Choiceable Supported element type * @see ArrayChoice Used when an array is given as parameter - * @see LazzyChoice Used when a method name is given as parameter + * @see LazyChoice Used when a method name is given as parameter */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Choices implements ChildBuilderAttributeInterface @@ -114,7 +114,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ // Q&D fix for psalm because it does not recognize trait as type /** @var StringElementBuilder $builder */ $builder->choices( - is_string($this->choices) ? new LazzyChoice([$form, $this->choices]) : $this->choices, + is_string($this->choices) ? new LazyChoice([$form, $this->choices]) : $this->choices, $options ); } @@ -131,8 +131,8 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen } if (is_string($this->choices)) { - $generator->use(LazzyChoice::class); - $choices = new Literal('new LazzyChoice([$form, ?])', [$this->choices]); + $generator->use(LazyChoice::class); + $choices = new Literal('new LazyChoice([$form, ?])', [$this->choices]); } else { $choices = $this->choices; } diff --git a/tests/Element/ChoicesTest.php b/tests/Element/ChoicesTest.php index d455d09..bbd68ab 100644 --- a/tests/Element/ChoicesTest.php +++ b/tests/Element/ChoicesTest.php @@ -74,7 +74,7 @@ public function generateChoices() use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Attribute\Processor\PostConfigureInterface; -use Bdf\Form\Choice\LazzyChoice; +use Bdf\Form\Choice\LazyChoice; use Bdf\Form\Leaf\StringElement; class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface @@ -91,7 +91,7 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $bar->choices(['foo', 'bar', 'baz'], ['min' => 2, 'multipleMessage' => 'my error', 'message' => 'my error']); $baz = $builder->add('baz', StringElement::class); - $baz->choices(new LazzyChoice([$form, 'generateChoices']), []); + $baz->choices(new LazyChoice([$form, 'generateChoices']), []); return $this; } From 7fceed05abe2303a2e10c15eed4027078f2d5fbc Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 9 Dec 2021 15:06:53 +0100 Subject: [PATCH 19/40] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index e842942..85569b0 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,12 @@ "minimum-stability": "dev", "require": { "php": "8.0.*", - "b2pweb/bdf-form": "dev-prepare-attributes", + "b2pweb/bdf-form": "~1.1", "nette/php-generator": "~3.6" }, "require-dev": { "phpunit/phpunit": "~9.5", - "vimeo/psalm": "dev-master", + "vimeo/psalm": "~4.15", "squizlabs/php_codesniffer": "~3.6.1", "symfony/security-csrf": "~6.1" }, From b509238478fd5a64ef0e0e6e5e36b4ae368dea90 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 10 Dec 2021 11:58:50 +0100 Subject: [PATCH 20/40] Define branch alias to v1.0 --- composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/composer.json b/composer.json index 85569b0..f9157c9 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,11 @@ "squizlabs/php_codesniffer": "~3.6.1", "symfony/security-csrf": "~6.1" }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "scripts": { "tests": "phpunit", "tests-with-coverage": "phpunit --coverage-clover coverage.xml", From 61bb761018c10e534b67f4b7a513b187a9658d8f Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 1 Feb 2022 16:27:50 +0100 Subject: [PATCH 21/40] PHP 8.1 compatibility --- .github/workflows/php.yml | 2 +- composer.json | 4 ++-- src/AttributeForm.php | 3 +++ src/Child/CallbackModelTransformer.php | 2 ++ src/Child/Configure.php | 2 ++ src/Child/DefaultValue.php | 2 ++ src/Child/Dependencies.php | 2 ++ src/Child/GetSet.php | 2 ++ src/Child/ModelTransformer.php | 2 ++ src/Constraint/CallbackConstraint.php | 2 ++ src/Constraint/Satisfy.php | 2 ++ src/Element/CallbackTransformer.php | 2 ++ src/Element/Choices.php | 2 ++ src/Element/Transformer.php | 2 ++ tests/Child/ModelTransformerTest.php | 11 +++++------ tests/Constraint/CallbackConstraintTest.php | 2 +- 16 files changed, 34 insertions(+), 10 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 6ef0f38..8a51569 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.0'] + php-versions: ['8.0', '8.1'] name: PHP ${{ matrix.php-versions }} steps: diff --git a/composer.json b/composer.json index f9157c9..5070380 100644 --- a/composer.json +++ b/composer.json @@ -22,13 +22,13 @@ }, "minimum-stability": "dev", "require": { - "php": "8.0.*", + "php": "~8.0.0 || ~8.1.0", "b2pweb/bdf-form": "~1.1", "nette/php-generator": "~3.6" }, "require-dev": { "phpunit/phpunit": "~9.5", - "vimeo/psalm": "~4.15", + "vimeo/psalm": "~4.19", "squizlabs/php_codesniffer": "~3.6.1", "symfony/security-csrf": "~6.1" }, diff --git a/src/AttributeForm.php b/src/AttributeForm.php index f31fb80..64d7b8b 100644 --- a/src/AttributeForm.php +++ b/src/AttributeForm.php @@ -12,6 +12,9 @@ /** * Utility class for declare a form using PHP 8 attributes and declare elements using typed properties + * + * @template T + * @extends CustomForm */ abstract class AttributeForm extends CustomForm { diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index 08f008f..2263e11 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -65,6 +65,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ChildBuilderInterface::modelTransformer() The called method * @see ModelTransformer For use a transformer class as model transformer * @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one diff --git a/src/Child/Configure.php b/src/Child/Configure.php index 7ed2fdd..bd9e87d 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -28,6 +28,8 @@ * } * } * + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] class Configure implements ChildBuilderAttributeInterface diff --git a/src/Child/DefaultValue.php b/src/Child/DefaultValue.php index 6e5aec3..722c114 100644 --- a/src/Child/DefaultValue.php +++ b/src/Child/DefaultValue.php @@ -29,6 +29,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ChildBuilderInterface::default() The called method */ #[Attribute(Attribute::TARGET_PROPERTY)] diff --git a/src/Child/Dependencies.php b/src/Child/Dependencies.php index 2948d27..a68f69b 100644 --- a/src/Child/Dependencies.php +++ b/src/Child/Dependencies.php @@ -28,6 +28,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ChildBuilderInterface::depends() The called method */ #[Attribute(Attribute::TARGET_PROPERTY)] diff --git a/src/Child/GetSet.php b/src/Child/GetSet.php index 2738c94..4151353 100644 --- a/src/Child/GetSet.php +++ b/src/Child/GetSet.php @@ -29,6 +29,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ChildBuilder::getter() * @see ChildBuilder::setter() * @see ChildBuilderInterface::hydrator() diff --git a/src/Child/ModelTransformer.php b/src/Child/ModelTransformer.php index c38c478..4016613 100644 --- a/src/Child/ModelTransformer.php +++ b/src/Child/ModelTransformer.php @@ -28,6 +28,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ChildBuilderInterface::modelTransformer() The called method * @see CallbackModelTransformer For use custom methods as transformer instead of class */ diff --git a/src/Constraint/CallbackConstraint.php b/src/Constraint/CallbackConstraint.php index a79e7ce..aa1a4fb 100644 --- a/src/Constraint/CallbackConstraint.php +++ b/src/Constraint/CallbackConstraint.php @@ -37,6 +37,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ElementBuilderInterface::satisfy() The called method * @see Constraint * @see Closure The used constraint diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index 1c0d706..0a16f9a 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -30,6 +30,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ElementBuilderInterface::satisfy() The called method * @see Constraint */ diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index f28e15b..018d260 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -68,6 +68,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ElementBuilderInterface::transformer() The called method * @see Transformer For use a transformer class as transformer * @see CallbackModelTransformer For use transformer in same way, but for model transformer intead of http one diff --git a/src/Element/Choices.php b/src/Element/Choices.php index 5a8aad0..7c31584 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -60,6 +60,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ChoiceBuilderTrait::choices() The called method * @see Choiceable Supported element type * @see ArrayChoice Used when an array is given as parameter diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index 9602426..97916cb 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -31,6 +31,8 @@ * } * * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * * @see ElementBuilderInterface::transformer() The called method * @see CallbackTransformer For use custom methods as transformer instead of class */ diff --git a/tests/Child/ModelTransformerTest.php b/tests/Child/ModelTransformerTest.php index a876cfe..59031d7 100644 --- a/tests/Child/ModelTransformerTest.php +++ b/tests/Child/ModelTransformerTest.php @@ -131,12 +131,12 @@ class ATransformer implements TransformerInterface { public function transformToHttp($value, ElementInterface $input) { - return base64_decode($value); + return base64_decode((string) $value); } public function transformFromHttp($value, ElementInterface $input) { - return base64_encode($value); + return base64_encode((string) $value); } } @@ -144,12 +144,12 @@ class BTransformer implements TransformerInterface { public function transformToHttp($value, ElementInterface $input) { - return hexdec($value); + return $value ? hexdec($value) : 0; } public function transformFromHttp($value, ElementInterface $input) { - return dechex($value); + return $value ? dechex($value) : ''; } } @@ -159,12 +159,11 @@ public function __construct(public string $prefix) {} public function transformToHttp($value, ElementInterface $input) { - return substr($value, strlen($this->prefix)); + return substr($value, strlen((string) $this->prefix)); } public function transformFromHttp($value, ElementInterface $input) { return $this->prefix . $value; } - } diff --git a/tests/Constraint/CallbackConstraintTest.php b/tests/Constraint/CallbackConstraintTest.php index 8897442..536a2fb 100644 --- a/tests/Constraint/CallbackConstraintTest.php +++ b/tests/Constraint/CallbackConstraintTest.php @@ -24,7 +24,7 @@ public function test(AttributesProcessorInterface $processor) public function validateFoo($value): bool { - return strlen($value) % 2 === 0; + return strlen((string) $value) % 2 === 0; } }; From cdc30db9444127359bcd1c121cb73ac3d8be6ab0 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 6 Jan 2023 13:50:22 +0100 Subject: [PATCH 22/40] chore: Compatibility with PHP 8.2 --- .github/workflows/php.yml | 11 ++++++++--- composer.json | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 8a51569..08b5710 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.0', '8.1'] + php-versions: ['8.0', '8.1', '8.2'] name: PHP ${{ matrix.php-versions }} steps: @@ -53,7 +53,12 @@ jobs: ini-values: date.timezone=Europe/Paris - name: Install Infection - run: composer global require infection/infection + run: | + wget https://github.com/infection/infection/releases/download/0.21.5/infection.phar + wget https://github.com/infection/infection/releases/download/0.21.5/infection.phar.asc + chmod +x infection.phar + gpg --recv-keys C6D76C329EBADE2FB9C458CFC5095986493B4AA0 + gpg --with-fingerprint --verify infection.phar.asc infection.phar - name: Validate composer.json and composer.lock run: composer validate --strict @@ -70,4 +75,4 @@ jobs: - name: Run Infection run: | git fetch --depth=1 origin $GITHUB_BASE_REF - ~/.composer/vendor/bin/infection --logger-github --git-diff-filter=AM + ./infection.phar --logger-github --git-diff-filter=AM diff --git a/composer.json b/composer.json index 5070380..57838ee 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ }, "minimum-stability": "dev", "require": { - "php": "~8.0.0 || ~8.1.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", "b2pweb/bdf-form": "~1.1", "nette/php-generator": "~3.6" }, From 05c33a00855b11badbbcaaed55751707ae2c3be7 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 6 Jan 2023 13:56:59 +0100 Subject: [PATCH 23/40] ci: fix composer for PHP 8.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 57838ee..ca625f7 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "phpunit/phpunit": "~9.5", "vimeo/psalm": "~4.19", "squizlabs/php_codesniffer": "~3.6.1", - "symfony/security-csrf": "~6.1" + "symfony/security-csrf": "~5.4|~6.1" }, "extra": { "branch-alias": { From 5e7951d5877f0027e62beb5010c78a46386e17a0 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 28 Feb 2024 12:20:13 +0100 Subject: [PATCH 24/40] chore: Compatibility with PHP 8.3 --- .github/workflows/php.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 08b5710..252913b 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.0', '8.1', '8.2'] + php-versions: ['8.0', '8.1', '8.2', '8.3'] name: PHP ${{ matrix.php-versions }} steps: diff --git a/composer.json b/composer.json index ca625f7..89c50d4 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ }, "minimum-stability": "dev", "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "b2pweb/bdf-form": "~1.1", "nette/php-generator": "~3.6" }, From 2b2f8dfd6bd144b9da7ff75113bf6370ef412255 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 28 Feb 2024 13:38:42 +0100 Subject: [PATCH 25/40] chore: Compatibility nette generator 4 --- composer.json | 2 +- tests/Child/CallbackModelTransformerTest.php | 10 ++++++---- tests/Element/CallbackTransformerTest.php | 5 +++-- .../CodeGenerator/TransformerClassGeneratorTest.php | 5 +++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 89c50d4..dee09ff 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require": { "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "b2pweb/bdf-form": "~1.1", - "nette/php-generator": "~3.6" + "nette/php-generator": "~3.6|~4.0" }, "require-dev": { "phpunit/phpunit": "~9.5", diff --git a/tests/Child/CallbackModelTransformerTest.php b/tests/Child/CallbackModelTransformerTest.php index 91e5fda..742ff66 100644 --- a/tests/Child/CallbackModelTransformerTest.php +++ b/tests/Child/CallbackModelTransformerTest.php @@ -133,8 +133,9 @@ function transformFromHttp($value, ElementInterface $input) return $this->form->t($value, $input); } - public function __construct(private $form) - { + public function __construct( + private $form, + ) { } }); $foo->extractor(new Getter()); @@ -158,8 +159,9 @@ function transformFromHttp($value, ElementInterface $input) return $value; } - public function __construct(private $form) - { + public function __construct( + private $form, + ) { } }); $bar->extractor(new Getter()); diff --git a/tests/Element/CallbackTransformerTest.php b/tests/Element/CallbackTransformerTest.php index 26849fb..2ff5c9d 100644 --- a/tests/Element/CallbackTransformerTest.php +++ b/tests/Element/CallbackTransformerTest.php @@ -145,8 +145,9 @@ function transformFromHttp($value, ElementInterface $input) return $this->form->inTransformer($value, $input); } - public function __construct(private $form) - { + public function __construct( + private $form, + ) { } }); diff --git a/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php b/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php index fd98c49..9a544d4 100644 --- a/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php +++ b/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php @@ -72,8 +72,9 @@ function transformFromHttp($value, ElementInterface $input) { } - public function __construct(private \Bdf\Form\Aggregate\FormInterface $foo) - { + public function __construct( + private \Bdf\Form\Aggregate\FormInterface $foo, + ) { } } PHP From 4f3d9c968e54b5fecb1c17138be1ca7fd48d52fd Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Mon, 4 Mar 2024 12:16:44 +0100 Subject: [PATCH 26/40] chore: Update to psalm 5 --- composer.json | 2 +- psalm.xml | 2 ++ src/Aggregate/ArrayConstraint.php | 1 + src/Aggregate/CallbackArrayConstraint.php | 6 ++++-- src/Aggregate/Count.php | 2 ++ src/Aggregate/ElementType.php | 6 ++++-- src/AttributeForm.php | 2 ++ src/Button/Groups.php | 2 ++ src/Button/Value.php | 2 ++ src/Child/CallbackModelTransformer.php | 14 ++++++++------ src/Child/Configure.php | 2 ++ src/Child/DefaultValue.php | 2 ++ src/Child/Dependencies.php | 2 ++ src/Child/GetSet.php | 2 ++ src/Child/ModelTransformer.php | 2 ++ src/Constraint/CallbackConstraint.php | 6 ++++-- src/Constraint/Satisfy.php | 2 ++ src/Element/CallbackTransformer.php | 14 ++++++++------ src/Element/Choices.php | 6 ++++-- src/Element/Date/DateFormat.php | 2 ++ src/Element/Date/DateTimeClass.php | 2 ++ src/Element/Date/ImmutableDateTime.php | 2 ++ src/Element/Date/Timezone.php | 2 ++ src/Element/IgnoreTransformerException.php | 2 ++ src/Element/Raw.php | 2 ++ src/Element/Transformer.php | 2 ++ src/Element/TransformerError.php | 14 ++++++++------ src/Form/CallbackGenerator.php | 2 ++ src/Form/Csrf.php | 2 ++ src/Form/Generates.php | 2 ++ src/Processor/AttributesProcessorInterface.php | 2 ++ .../CodeGenerator/AttributesProcessorGenerator.php | 2 +- src/Processor/CompileAttributesProcessor.php | 5 +++++ src/Processor/ReflectionProcessor.php | 3 ++- 34 files changed, 94 insertions(+), 29 deletions(-) diff --git a/composer.json b/composer.json index dee09ff..d8d896f 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ }, "require-dev": { "phpunit/phpunit": "~9.5", - "vimeo/psalm": "~4.19", + "vimeo/psalm": "~5.22", "squizlabs/php_codesniffer": "~3.6.1", "symfony/security-csrf": "~5.4|~6.1" }, diff --git a/psalm.xml b/psalm.xml index 3240886..6d7bfee 100644 --- a/psalm.xml +++ b/psalm.xml @@ -2,6 +2,8 @@ + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class ArrayConstraint implements ChildBuilderAttributeInterface diff --git a/src/Aggregate/CallbackArrayConstraint.php b/src/Aggregate/CallbackArrayConstraint.php index 1738428..88986cb 100644 --- a/src/Aggregate/CallbackArrayConstraint.php +++ b/src/Aggregate/CallbackArrayConstraint.php @@ -42,6 +42,8 @@ * @see ArrayConstraint Use for a class constraint * * @implements ChildBuilderAttributeInterface + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class CallbackArrayConstraint implements ChildBuilderAttributeInterface @@ -82,7 +84,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $constraint = new Closure(['callback' => [$form, $this->methodName]]); - if ($this->message) { + if ($this->message !== null) { $constraint->message = $this->message; } @@ -96,7 +98,7 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen { $generator->use(Closure::class, 'ClosureConstraint'); - $parameters = $this->message + $parameters = $this->message !== null ? new Literal("['callback' => [\$form, ?], 'message' => ?]", [$this->methodName, $this->message]) : new Literal('[$form, ?]', [$this->methodName]) ; diff --git a/src/Aggregate/Count.php b/src/Aggregate/Count.php index a303471..9dd9d10 100644 --- a/src/Aggregate/Count.php +++ b/src/Aggregate/Count.php @@ -34,6 +34,8 @@ * * @implements ChildBuilderAttributeInterface * + * @api + * * @psalm-suppress PropertyNotSetInConstructor */ #[Attribute(Attribute::TARGET_PROPERTY)] diff --git a/src/Aggregate/ElementType.php b/src/Aggregate/ElementType.php index e9d07cb..171ea0d 100644 --- a/src/Aggregate/ElementType.php +++ b/src/Aggregate/ElementType.php @@ -41,6 +41,8 @@ * @see ArrayElementBuilder::element() The called method * * @implements ChildBuilderAttributeInterface + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] class ElementType implements ChildBuilderAttributeInterface @@ -69,7 +71,7 @@ public function __construct( */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - $configurator = $this->configurator ? [$form, $this->configurator] : null; + $configurator = $this->configurator !== null ? [$form, $this->configurator] : null; $builder->element($this->elementType, $configurator); } @@ -80,7 +82,7 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen { $elementType = new Literal($generator->useAndSimplifyType($this->elementType)); - if ($this->configurator) { + if ($this->configurator !== null) { $generator->line('$?->element(?::class, [$form, ?]);', [$name, $elementType, $this->configurator]); } else { $generator->line('$?->element(?::class);', [$name, $elementType]); diff --git a/src/AttributeForm.php b/src/AttributeForm.php index 64d7b8b..c4ca0d8 100644 --- a/src/AttributeForm.php +++ b/src/AttributeForm.php @@ -15,6 +15,8 @@ * * @template T * @extends CustomForm + * + * @api */ abstract class AttributeForm extends CustomForm { diff --git a/src/Button/Groups.php b/src/Button/Groups.php index d0a0597..5df3540 100644 --- a/src/Button/Groups.php +++ b/src/Button/Groups.php @@ -32,6 +32,8 @@ * @see ButtonBuilderInterface::groups() The called method * @see ButtonInterface::constraintGroups() Modify this value * @see RootElementInterface::constraintGroups() + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Groups implements ButtonBuilderAttributeInterface diff --git a/src/Button/Value.php b/src/Button/Value.php index 4f713bd..5d76312 100644 --- a/src/Button/Value.php +++ b/src/Button/Value.php @@ -28,6 +28,8 @@ * * * @see ButtonBuilderInterface::value() The called method + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Value implements ButtonBuilderAttributeInterface diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index 2263e11..65f8e21 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -70,6 +70,8 @@ * @see ChildBuilderInterface::modelTransformer() The called method * @see ModelTransformer For use a transformer class as model transformer * @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class CallbackModelTransformer implements ChildBuilderAttributeInterface @@ -105,7 +107,7 @@ public function __construct( */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - if ($this->callback) { + if ($this->callback !== null) { $builder->modelTransformer([$form, $this->callback]); return; } @@ -123,7 +125,7 @@ public function __construct( */ public function transformToHttp($value, ElementInterface $input) { - if (!$this->toInput) { + if ($this->toInput === null) { return $value; } @@ -135,7 +137,7 @@ public function transformToHttp($value, ElementInterface $input) */ public function transformFromHttp($value, ElementInterface $input) { - if (!$this->toEntity) { + if ($this->toEntity === null) { return $value; } @@ -149,7 +151,7 @@ public function transformFromHttp($value, ElementInterface $input) */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - if ($this->callback) { + if ($this->callback !== null) { $generator->line('$?->modelTransformer([$form, ?]);', [$name, $this->callback]); return; } @@ -158,13 +160,13 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen $transformer->withPromotedProperty('form')->setPrivate(); - if ($this->toInput) { + if ($this->toInput !== null) { $transformer->toHttp()->setBody('return $this->form->?($value, $input);', [$this->toInput]); } else { $transformer->toHttp()->setBody('return $value;'); } - if ($this->toEntity) { + if ($this->toEntity !== null) { $transformer->fromHttp()->setBody('return $this->form->?($value, $input);', [$this->toEntity]); } else { $transformer->fromHttp()->setBody('return $value;'); diff --git a/src/Child/Configure.php b/src/Child/Configure.php index bd9e87d..caab40a 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -30,6 +30,8 @@ * * * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] class Configure implements ChildBuilderAttributeInterface diff --git a/src/Child/DefaultValue.php b/src/Child/DefaultValue.php index 722c114..2c82c5f 100644 --- a/src/Child/DefaultValue.php +++ b/src/Child/DefaultValue.php @@ -32,6 +32,8 @@ * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> * * @see ChildBuilderInterface::default() The called method + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class DefaultValue implements ChildBuilderAttributeInterface diff --git a/src/Child/Dependencies.php b/src/Child/Dependencies.php index a68f69b..adbc5b6 100644 --- a/src/Child/Dependencies.php +++ b/src/Child/Dependencies.php @@ -31,6 +31,8 @@ * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> * * @see ChildBuilderInterface::depends() The called method + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Dependencies implements ChildBuilderAttributeInterface diff --git a/src/Child/GetSet.php b/src/Child/GetSet.php index 4151353..58c38bc 100644 --- a/src/Child/GetSet.php +++ b/src/Child/GetSet.php @@ -38,6 +38,8 @@ * * @see Getter For define only the extractor * @see Setter For define only the hydrator + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class GetSet implements ChildBuilderAttributeInterface diff --git a/src/Child/ModelTransformer.php b/src/Child/ModelTransformer.php index 4016613..ba5ad1c 100644 --- a/src/Child/ModelTransformer.php +++ b/src/Child/ModelTransformer.php @@ -32,6 +32,8 @@ * * @see ChildBuilderInterface::modelTransformer() The called method * @see CallbackModelTransformer For use custom methods as transformer instead of class + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class ModelTransformer implements ChildBuilderAttributeInterface diff --git a/src/Constraint/CallbackConstraint.php b/src/Constraint/CallbackConstraint.php index aa1a4fb..e28ee96 100644 --- a/src/Constraint/CallbackConstraint.php +++ b/src/Constraint/CallbackConstraint.php @@ -42,6 +42,8 @@ * @see ElementBuilderInterface::satisfy() The called method * @see Constraint * @see Closure The used constraint + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class CallbackConstraint implements ChildBuilderAttributeInterface @@ -82,7 +84,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $constraint = new Closure(['callback' => [$form, $this->methodName]]); - if ($this->message) { + if ($this->message !== null) { $constraint->message = $this->message; } @@ -96,7 +98,7 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen { $generator->use(Closure::class, 'ClosureConstraint'); - $parameters = $this->message + $parameters = $this->message !== null ? new Literal("['callback' => [\$form, ?], 'message' => ?]", [$this->methodName, $this->message]) : new Literal('[$form, ?]', [$this->methodName]) ; diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index 0a16f9a..fa046d4 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -34,6 +34,8 @@ * * @see ElementBuilderInterface::satisfy() The called method * @see Constraint + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] class Satisfy implements ChildBuilderAttributeInterface diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index 018d260..330dfe2 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -73,6 +73,8 @@ * @see ElementBuilderInterface::transformer() The called method * @see Transformer For use a transformer class as transformer * @see CallbackModelTransformer For use transformer in same way, but for model transformer intead of http one + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class CallbackTransformer implements ChildBuilderAttributeInterface @@ -108,7 +110,7 @@ public function __construct( */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - if ($this->callback) { + if ($this->callback !== null) { $builder->transformer([$form, $this->callback]); return; } @@ -126,7 +128,7 @@ public function __construct( */ public function transformToHttp($value, ElementInterface $input) { - if (!$this->toHttp) { + if ($this->toHttp === null) { return $value; } @@ -138,7 +140,7 @@ public function transformToHttp($value, ElementInterface $input) */ public function transformFromHttp($value, ElementInterface $input) { - if (!$this->fromHttp) { + if ($this->fromHttp === null) { return $value; } @@ -152,7 +154,7 @@ public function transformFromHttp($value, ElementInterface $input) */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - if ($this->callback) { + if ($this->callback !== null) { $generator->line('$?->transformer([$form, ?]);', [$name, $this->callback]); return; } @@ -161,13 +163,13 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen $transformer->withPromotedProperty('form')->setPrivate(); - if ($this->toHttp) { + if ($this->toHttp !== null) { $transformer->toHttp()->setBody('return $this->form->?($value, $input);', [$this->toHttp]); } else { $transformer->toHttp()->setBody('return $value;'); } - if ($this->fromHttp) { + if ($this->fromHttp !== null) { $transformer->fromHttp()->setBody('return $this->form->?($value, $input);', [$this->fromHttp]); } else { $transformer->fromHttp()->setBody('return $value;'); diff --git a/src/Element/Choices.php b/src/Element/Choices.php index 7c31584..83c6936 100644 --- a/src/Element/Choices.php +++ b/src/Element/Choices.php @@ -66,6 +66,8 @@ * @see Choiceable Supported element type * @see ArrayChoice Used when an array is given as parameter * @see LazyChoice Used when a method name is given as parameter + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Choices implements ChildBuilderAttributeInterface @@ -109,7 +111,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ { $options = $this->options; - if ($this->message) { + if ($this->message !== null) { $options['message'] = $options['multipleMessage'] = $this->message; } @@ -128,7 +130,7 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen { $options = $this->options; - if ($this->message) { + if ($this->message !== null) { $options['message'] = $options['multipleMessage'] = $this->message; } diff --git a/src/Element/Date/DateFormat.php b/src/Element/Date/DateFormat.php index 124190e..dcb2bf5 100644 --- a/src/Element/Date/DateFormat.php +++ b/src/Element/Date/DateFormat.php @@ -30,6 +30,8 @@ * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::format() The called method * * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class DateFormat implements ChildBuilderAttributeInterface diff --git a/src/Element/Date/DateTimeClass.php b/src/Element/Date/DateTimeClass.php index 54f878d..7154d9f 100644 --- a/src/Element/Date/DateTimeClass.php +++ b/src/Element/Date/DateTimeClass.php @@ -31,6 +31,8 @@ * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::className() The called method * * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class DateTimeClass implements ChildBuilderAttributeInterface diff --git a/src/Element/Date/ImmutableDateTime.php b/src/Element/Date/ImmutableDateTime.php index c99bfe9..d6187ee 100644 --- a/src/Element/Date/ImmutableDateTime.php +++ b/src/Element/Date/ImmutableDateTime.php @@ -31,6 +31,8 @@ * @see DateTimeClass For use a custom class name * * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class ImmutableDateTime implements ChildBuilderAttributeInterface diff --git a/src/Element/Date/Timezone.php b/src/Element/Date/Timezone.php index 8791775..3ec7e9a 100644 --- a/src/Element/Date/Timezone.php +++ b/src/Element/Date/Timezone.php @@ -30,6 +30,8 @@ * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::timezone() The called method * * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Timezone implements ChildBuilderAttributeInterface diff --git a/src/Element/IgnoreTransformerException.php b/src/Element/IgnoreTransformerException.php index 1cbe1ad..0aed003 100644 --- a/src/Element/IgnoreTransformerException.php +++ b/src/Element/IgnoreTransformerException.php @@ -36,6 +36,8 @@ * @see ValidatorBuilderTrait::ignoreTransformerException() The called method * * @implements ChildBuilderAttributeInterface + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class IgnoreTransformerException implements ChildBuilderAttributeInterface diff --git a/src/Element/Raw.php b/src/Element/Raw.php index 30c41cc..7de263a 100644 --- a/src/Element/Raw.php +++ b/src/Element/Raw.php @@ -31,6 +31,8 @@ * @see NumberElementBuilder::raw() The called method * * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\NumberElementBuilder> + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Raw implements ChildBuilderAttributeInterface diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index 97916cb..85a7aed 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -35,6 +35,8 @@ * * @see ElementBuilderInterface::transformer() The called method * @see CallbackTransformer For use custom methods as transformer instead of class + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] class Transformer implements ChildBuilderAttributeInterface diff --git a/src/Element/TransformerError.php b/src/Element/TransformerError.php index e9c55d3..1c646ab 100644 --- a/src/Element/TransformerError.php +++ b/src/Element/TransformerError.php @@ -37,6 +37,8 @@ * @see ValidatorBuilderTrait::transformerExceptionValidation() The called method when validationCallback parameter is provided * * @implements ChildBuilderAttributeInterface + * + * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] class TransformerError implements ChildBuilderAttributeInterface @@ -75,15 +77,15 @@ public function __construct( */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - if ($this->message) { + if ($this->message !== null) { $builder->transformerErrorMessage($this->message); } - if ($this->code) { + if ($this->code !== null) { $builder->transformerErrorCode($this->code); } - if ($this->validationCallback) { + if ($this->validationCallback !== null) { $builder->transformerExceptionValidation([$form, $this->validationCallback]); } } @@ -95,15 +97,15 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen { $generator->line('$?', [$name]); - if ($this->message) { + if ($this->message !== null) { $generator->line(' ->transformerErrorMessage(?)', [$this->message]); } - if ($this->code) { + if ($this->code !== null) { $generator->line(' ->transformerErrorCode(?)', [$this->code]); } - if ($this->validationCallback) { + if ($this->validationCallback !== null) { $generator->line(' ->transformerExceptionValidation([$form, ?])', [$this->validationCallback]); } diff --git a/src/Form/CallbackGenerator.php b/src/Form/CallbackGenerator.php index ede5a92..eaaff19 100644 --- a/src/Form/CallbackGenerator.php +++ b/src/Form/CallbackGenerator.php @@ -35,6 +35,8 @@ * @see FormBuilderInterface::generates() The called method * @see ValueGenerator * @see Generates For generate with a simple class name + * + * @api */ #[Attribute(Attribute::TARGET_CLASS)] final class CallbackGenerator implements FormBuilderAttributeInterface diff --git a/src/Form/Csrf.php b/src/Form/Csrf.php index f028adb..13c8808 100644 --- a/src/Form/Csrf.php +++ b/src/Form/Csrf.php @@ -31,6 +31,8 @@ * * @see FormBuilderInterface::csrf() The called method * @see CsrfElementBuilder + * + * @api */ #[Attribute(Attribute::TARGET_CLASS)] final class Csrf implements FormBuilderAttributeInterface diff --git a/src/Form/Generates.php b/src/Form/Generates.php index 95ed8c1..900d259 100644 --- a/src/Form/Generates.php +++ b/src/Form/Generates.php @@ -32,6 +32,8 @@ * @see FormBuilderInterface::generates() The called method * @see ValueGenerator * @see CallbackGenerator For generate using a custom method + * + * @api */ #[Attribute(Attribute::TARGET_CLASS)] final class Generates implements FormBuilderAttributeInterface diff --git a/src/Processor/AttributesProcessorInterface.php b/src/Processor/AttributesProcessorInterface.php index fa410aa..9de13b1 100644 --- a/src/Processor/AttributesProcessorInterface.php +++ b/src/Processor/AttributesProcessorInterface.php @@ -7,6 +7,8 @@ /** * Process attributes of the form class to configure the inner form + * + * @api */ interface AttributesProcessorInterface { diff --git a/src/Processor/CodeGenerator/AttributesProcessorGenerator.php b/src/Processor/CodeGenerator/AttributesProcessorGenerator.php index b0ab9a0..a71c4fa 100644 --- a/src/Processor/CodeGenerator/AttributesProcessorGenerator.php +++ b/src/Processor/CodeGenerator/AttributesProcessorGenerator.php @@ -21,7 +21,7 @@ final class AttributesProcessorGenerator extends ClassGenerator */ public function __construct(string $className) { - if (!$classNamePos = strrpos($className, '\\')) { + if (($classNamePos = strrpos($className, '\\')) === false) { throw new InvalidArgumentException('The class name must have a namespace'); } diff --git a/src/Processor/CompileAttributesProcessor.php b/src/Processor/CompileAttributesProcessor.php index d4cfb48..84d81ef 100644 --- a/src/Processor/CompileAttributesProcessor.php +++ b/src/Processor/CompileAttributesProcessor.php @@ -16,6 +16,8 @@ * - Include the processor class file * - Instantiate the generated processor * - Delegate the form configuration to the generated processor + * + * @api */ final class CompileAttributesProcessor implements AttributesProcessorInterface { @@ -40,6 +42,8 @@ public function __construct( /** * {@inheritdoc} + * + * @psalm-suppress PossiblyUnusedReturnValue */ public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): PostConfigureInterface { @@ -47,6 +51,7 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil $className = ($this->classNameResolver)($form); if (!class_exists($className)) { + /** @psalm-suppress ArgumentTypeCoercion */ $this->loadProcessor($className, $form, $builder); } diff --git a/src/Processor/ReflectionProcessor.php b/src/Processor/ReflectionProcessor.php index d01f417..e620d80 100644 --- a/src/Processor/ReflectionProcessor.php +++ b/src/Processor/ReflectionProcessor.php @@ -14,6 +14,8 @@ * * The configuration action will be delegated to the ReflectionStrategyInterface * This implementation is only responsive of iterate over class hierarchy and properties + * + * @api */ final class ReflectionProcessor implements AttributesProcessorInterface { @@ -44,7 +46,6 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil $this->strategy->onFormClass($formClass, $form, $builder); foreach ($formClass->getProperties() as $property) { - /** @var non-empty-string $name */ $name = $property->getName(); if ( From a79cbb694d3de55bbca58f36e8a24c8c2eef3d41 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 26 Jul 2024 10:11:25 +0200 Subject: [PATCH 27/40] feat: Add new attributes + allow to annotation methods (#FRAM-173) --- phpunit.xml.dist | 4 + src/Aggregate/ArrayConstraint.php | 36 +++- src/Aggregate/ArrayTransformer.php | 45 +++++ src/Aggregate/AsArrayConstraint.php | 91 ++++++++++ src/Aggregate/CallbackArrayConstraint.php | 2 - src/Child/AsFilter.php | 74 ++++++++ src/Child/AsModelTransformer.php | 82 +++++++++ src/Child/CallbackFilter.php | 74 ++++++++ src/Child/CallbackModelTransformer.php | 7 +- src/Child/Configure.php | 50 +++++- src/Child/HttpField.php | 78 +++++++++ src/Constraint/AsConstraint.php | 91 ++++++++++ src/Constraint/CallbackConstraint.php | 3 +- src/Constraint/Satisfy.php | 34 +++- src/Element/AsTransformer.php | 82 +++++++++ src/Element/CallbackTransformer.php | 6 +- src/Element/Date/AfterField.php | 81 +++++++++ src/Element/Date/BeforeField.php | 81 +++++++++ src/Element/Required.php | 66 ++++++++ src/Element/Transformer.php | 30 +++- .../CodeGenerator/ObjectInstantiation.php | 92 ++++++++++ .../ConfigureFormBuilderStrategy.php | 14 +- .../GenerateConfiguratorStrategy.php | 22 ++- .../MethodChildBuilderAttributeInterface.php | 33 ++++ src/Processor/ProcessorMetadata.php | 93 ++++++++++ src/Processor/ReflectionProcessor.php | 45 +++-- src/Processor/ReflectionStrategyInterface.php | 17 +- tests/Aggregate/ArrayTransformerTest.php | 94 +++++++++++ tests/Aggregate/AsArrayConstraintTest.php | 108 ++++++++++++ tests/Child/AsFilterTest.php | 98 +++++++++++ tests/Child/AsModelTransformerTest.php | 96 +++++++++++ tests/Child/CallbackFilterTest.php | 90 ++++++++++ tests/Child/ConfigureTest.php | 78 +++++++++ tests/Child/HttpFieldTest.php | 82 +++++++++ tests/Constraint/AsConstraintTest.php | 100 +++++++++++ tests/Element/AsTransformerTest.php | 85 ++++++++++ tests/Element/Date/AfterFieldTest.php | 158 +++++++++++++++++ tests/Element/Date/BeforeFieldTest.php | 159 ++++++++++++++++++ tests/Element/RequiredTest.php | 84 +++++++++ tests/Element/TransformerTest.php | 81 +++++++++ tests/FunctionalTest.php | 84 ++++++++- tests/Php81/Aggregate/ArrayConstraintTest.php | 97 +++++++++++ .../Aggregate/CallbackArrayConstraintTest.php | 108 ++++++++++++ tests/Php81/Constraint/SatisfyTest.php | 76 +++++++++ .../CodeGenerator/ObjectInstantiationTest.php | 40 +++++ .../GenerateConfiguratorStrategyTest.php | 2 - 46 files changed, 2990 insertions(+), 63 deletions(-) create mode 100644 src/Aggregate/ArrayTransformer.php create mode 100644 src/Aggregate/AsArrayConstraint.php create mode 100644 src/Child/AsFilter.php create mode 100644 src/Child/AsModelTransformer.php create mode 100644 src/Child/CallbackFilter.php create mode 100644 src/Child/HttpField.php create mode 100644 src/Constraint/AsConstraint.php create mode 100644 src/Element/AsTransformer.php create mode 100644 src/Element/Date/AfterField.php create mode 100644 src/Element/Date/BeforeField.php create mode 100644 src/Element/Required.php create mode 100644 src/Processor/CodeGenerator/ObjectInstantiation.php create mode 100644 src/Processor/MethodChildBuilderAttributeInterface.php create mode 100644 src/Processor/ProcessorMetadata.php create mode 100644 tests/Aggregate/ArrayTransformerTest.php create mode 100644 tests/Aggregate/AsArrayConstraintTest.php create mode 100644 tests/Child/AsFilterTest.php create mode 100644 tests/Child/AsModelTransformerTest.php create mode 100644 tests/Child/CallbackFilterTest.php create mode 100644 tests/Child/HttpFieldTest.php create mode 100644 tests/Constraint/AsConstraintTest.php create mode 100644 tests/Element/AsTransformerTest.php create mode 100644 tests/Element/Date/AfterFieldTest.php create mode 100644 tests/Element/Date/BeforeFieldTest.php create mode 100644 tests/Element/RequiredTest.php create mode 100644 tests/Php81/Aggregate/ArrayConstraintTest.php create mode 100644 tests/Php81/Aggregate/CallbackArrayConstraintTest.php create mode 100644 tests/Php81/Constraint/SatisfyTest.php create mode 100644 tests/Processor/CodeGenerator/ObjectInstantiationTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a9e870d..3b8f2ad 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,10 @@ ./tests + ./tests/Php81 + + + ./tests/Php81 diff --git a/src/Aggregate/ArrayConstraint.php b/src/Aggregate/ArrayConstraint.php index d73332f..f6117fd 100644 --- a/src/Aggregate/ArrayConstraint.php +++ b/src/Aggregate/ArrayConstraint.php @@ -8,11 +8,15 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Constraint\Satisfy; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation; use Bdf\Form\Child\ChildBuilderInterface; +use InvalidArgumentException; use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; +use function is_object; +use function is_string; + /** * Add a constraint on the whole array element * Use Satisfy, or directly the constraint as attribute for add a constraint on one array item @@ -28,6 +32,10 @@ * { * #[ArrayConstraint(Unique::class, ['message' => 'My error'])] * private ArrayElement $values; + * + * // or on PHP 8.1 + * #[ArrayConstraint(new Unique(['message' => 'My error']))] + * private ArrayElement $values; * } * * @@ -43,12 +51,19 @@ final class ArrayConstraint implements ChildBuilderAttributeInterface { public function __construct( /** - * The constraint class name + * The constraint * - * @var class-string + * You can use a class name, and provider arguments on the next parameter, + * or directly use the constraint instance. + * + * When a constraint instance is used, in case of code generation, + * the constructor parameters will be deduced from public properties of the constraint. + * This may not work if the constraint has a complex constructor. + * + * @var class-string|Constraint * @readonly */ - private string $constraint, + private string|Constraint $constraint, /** * Constraint's constructor options * @@ -57,6 +72,9 @@ public function __construct( */ private mixed $options = null ) { + if (is_object($constraint) && $options !== null) { + throw new InvalidArgumentException('Cannot use options with constraint instance'); + } } /** @@ -72,8 +90,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - $constraint = $generator->useAndSimplifyType($this->constraint); - - $generator->line('$?->arrayConstraint(?::class, ?);', [$name, new Literal($constraint), $this->options]); + if (is_string($this->constraint)) { + $constraint = $generator->useAndSimplifyType($this->constraint); + $generator->line('$?->arrayConstraint(?::class, ?);', [$name, new Literal($constraint), $this->options]); + } else { + $constraint = ObjectInstantiation::singleArrayParameter($this->constraint)->render($generator); + $generator->line('$?->arrayConstraint(?);', [$name, $constraint]); + } } } diff --git a/src/Aggregate/ArrayTransformer.php b/src/Aggregate/ArrayTransformer.php new file mode 100644 index 0000000..7dfef5f --- /dev/null +++ b/src/Aggregate/ArrayTransformer.php @@ -0,0 +1,45 @@ + + * $builder->string('foo')->arrayTransformer(new MyTransformer(...$arguments)); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[ArrayTransformer(MyTransformer::class, ['foo', 'bar']), ElementType(IntegerElement::class)] + * private ArrayElement $foo; + * } + * + * + * @see ArrayElementBuilder::arrayTransformer() The called method + * @see CallbackTransformer For use custom methods as transformer instead of class + * @see Transformer To add a transformer the item of the array + * + * @api + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +class ArrayTransformer extends Transformer +{ + /** + * @param class-string $transformerClass The transformer class name + * @param array $constructorArguments Arguments to provide on the transformer constructor + */ + public function __construct(string $transformerClass, array $constructorArguments = []) + { + parent::__construct($transformerClass, $constructorArguments, true); + } +} diff --git a/src/Aggregate/AsArrayConstraint.php b/src/Aggregate/AsArrayConstraint.php new file mode 100644 index 0000000..b2ea42c --- /dev/null +++ b/src/Aggregate/AsArrayConstraint.php @@ -0,0 +1,91 @@ + + * $builder->array('foo')->arrayConstraint([$this, 'validateFoo'], 'Foo is invalid'); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * private ArrayElement $foo; + * + * #[AsArrayConstraint('foo', message: 'Foo is invalid')] + * public function validateFoo(array $value, ElementInterface $input): bool + * { + * return count($value) % 5 > 2; + * } + * } + * + * + * @see ArrayElementBuilder::arrayConstraint() The called method + * @see Constraint + * @see Closure The used constraint + * @see ArrayConstraint Use for a class constraint + * @see CallbackArrayConstraint To annotate the property instead of the method + * + * @implements MethodChildBuilderAttributeInterface + * + * @api + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +final class AsArrayConstraint implements MethodChildBuilderAttributeInterface +{ + public function __construct( + /** + * The element property name to which the constraint is applied + * + * @var literal-string + * @readonly + */ + private string $target, + /** + * The error message to use + * This option is used only if the validator return false, in other cases, + * the message returned by the validator will be used + * + * @var string|null + * @readonly + */ + private ?string $message = null, + ) { + } + + /** + * {@inheritdoc} + */ + public function targetElements(): array + { + return [$this->target]; + } + + /** + * {@inheritdoc} + */ + public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface + { + return new CallbackArrayConstraint($method->getName(), $this->message); + } +} diff --git a/src/Aggregate/CallbackArrayConstraint.php b/src/Aggregate/CallbackArrayConstraint.php index 88986cb..1be3d49 100644 --- a/src/Aggregate/CallbackArrayConstraint.php +++ b/src/Aggregate/CallbackArrayConstraint.php @@ -7,10 +7,8 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Constraint\Closure; -use Bdf\Form\ElementBuilderInterface; use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; diff --git a/src/Child/AsFilter.php b/src/Child/AsFilter.php new file mode 100644 index 0000000..cdc704b --- /dev/null +++ b/src/Child/AsFilter.php @@ -0,0 +1,74 @@ + + * $builder->string('foo')->filter([$this, 'myTransformer']); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * private IntegerElement $foo; + * + * #[AsFilter('filterFoo')] + * public function filterFoo($value, ChildInterface $child, $default): string + * { + * return hexdec($value); + * } + * } + * + * + * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * + * @see ChildBuilderInterface::filter() The called method + * @see ClosureFilter The used filter class + * @see CallbackTransformer For use transformer in same way, but for http transformer intead of filter one + * @see CallbackFilter To annotate the property instead + * + * @api + */ +#[Attribute(Attribute::TARGET_METHOD)] +final class AsFilter implements MethodChildBuilderAttributeInterface +{ + /** + * @var list + * @readonly + */ + private array $targets; + + /** + * @param non-empty-string ...$targets List of elements names that the attribute can be applied on. Must be a property name. + * @no-named-arguments + */ + public function __construct(string ...$targets) + { + $this->targets = $targets; + } + + /** + * {@inheritdoc} + */ + public function targetElements(): array + { + return $this->targets; + } + + /** + * {@inheritdoc} + */ + public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface + { + return new CallbackFilter($method->getName()); + } +} diff --git a/src/Child/AsModelTransformer.php b/src/Child/AsModelTransformer.php new file mode 100644 index 0000000..6a85f06 --- /dev/null +++ b/src/Child/AsModelTransformer.php @@ -0,0 +1,82 @@ + + * $builder->string('foo')->modelTransformer([$this, 'myTransformer']); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * private IntegerElement $bar; + * + * #[AsModelTransformer('bar')] + * public function barTransformer($value, IntegerElement $input, bool $toEntity) + * { + * return $toEntity ? dechex($value) : hexdec($value); + * } + * } + * + * + * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * + * @see ChildBuilderInterface::modelTransformer() The called method + * @see ModelTransformer For use a transformer class as model transformer + * @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one + * @see CallbackModelTransformer To annotate the property instead + * + * @api + */ +#[Attribute(Attribute::TARGET_METHOD)] +final class AsModelTransformer implements MethodChildBuilderAttributeInterface +{ + /** + * @var list + * @readonly + */ + private array $targets; + + /** + * @param non-empty-string ...$targets Target elements properties names + * @no-named-arguments + */ + public function __construct(string ...$targets) + { + $this->targets = $targets; + } + + /** + * {@inheritdoc} + */ + public function targetElements(): array + { + return $this->targets; + } + + /** + * {@inheritdoc} + */ + public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface + { + return new CallbackModelTransformer($method->getName()); + } +} diff --git a/src/Child/CallbackFilter.php b/src/Child/CallbackFilter.php new file mode 100644 index 0000000..3c0aace --- /dev/null +++ b/src/Child/CallbackFilter.php @@ -0,0 +1,74 @@ + + * $builder->string('foo')->filter([$this, 'myTransformer']); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[CallbackFilter('filterFoo')] + * private IntegerElement $foo; + * + * public function filterFoo($value, ChildInterface $child, $default): string + * { + * return hexdec($value); + * } + * } + * + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * + * @see ChildBuilderInterface::filter() The called method + * @see ClosureFilter The used filter class + * @see CallbackTransformer For use transformer in same way, but for http transformer intead of filter one + * @see AsFilter To annotate a method as filter + * + * @api + */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class CallbackFilter implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The method name to use as filter + * + * The method must be public and follow the signature `function (mixed $value, ElementInterface $input, mixed|null $default): mixed` + * + * @var non-empty-string + * @readonly + */ + private string $method, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->filter([$form, $this->method]); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->filter([$form, ?]);', [$name, $this->method]); + } +} diff --git a/src/Child/CallbackModelTransformer.php b/src/Child/CallbackModelTransformer.php index 65f8e21..701d080 100644 --- a/src/Child/CallbackModelTransformer.php +++ b/src/Child/CallbackModelTransformer.php @@ -70,6 +70,7 @@ * @see ChildBuilderInterface::modelTransformer() The called method * @see ModelTransformer For use a transformer class as model transformer * @see CallbackTransformer For use transformer in same way, but for http transformer intead of model one + * @see AsModelTransformer To annotate the method instead * * @api */ @@ -81,21 +82,21 @@ public function __construct( * Method name use to define the unified transformer method * If defined, the other parameters will be ignored * - * @var literal-string|null + * @var non-empty-string|null * @readonly */ private ?string $callback = null, /** * Method name use to define the transformation process from input value to the entity * - * @var literal-string|null + * @var non-empty-string|null * @readonly */ private ?string $toEntity = null, /** * Method name use to define the transformation process from entity value to input * - * @var literal-string|null + * @var non-empty-string|null * @readonly */ private ?string $toInput = null, diff --git a/src/Child/Configure.php b/src/Child/Configure.php index caab40a..c5f6a82 100644 --- a/src/Child/Configure.php +++ b/src/Child/Configure.php @@ -6,14 +6,18 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface; use Bdf\Form\Child\ChildBuilderInterface; +use ReflectionMethod; /** * Define a custom configuration method for an element * * Use this attribute when other attributes cannot be used to configure the current element - * Takes as argument the method name to use. This method must be declared into the form class with public visibility + * Takes as argument the method name to use. This method must be declared into the form class with public visibility. + * + * It can also be used to directly annotate the configuration method. + * In this case, it will take as argument the property name to configure. * * Usage: * @@ -26,25 +30,37 @@ * { * $builder->min(5); * } + * + * // Or directly on the method (same as above) + * #[Configure('foo')] + * public function otherConfiguration(ChildBuilderInterface $builder): void + * { + * $builder->min(5); + * } * } * * * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> * * @api */ -#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -class Configure implements ChildBuilderAttributeInterface +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class Configure implements ChildBuilderAttributeInterface, MethodChildBuilderAttributeInterface { public function __construct( /** - * The method name to use as configurator + * Thet target of the configuration. + * + * In case of a property attribute, define the method name to use as configurator. * The method should follow the prototype `public function (ChildBuilderInterface $builder): void` * - * @var literal-string + * In case of a method attribute, define the property name to configure. + * + * @var non-empty-string * @readonly */ - private string $callback + private string $target ) { } @@ -53,7 +69,7 @@ public function __construct( */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - $form->{$this->callback}($builder); + $form->{$this->target}($builder); } /** @@ -61,6 +77,22 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - $generator->line('$form->?($?);', [$this->callback, $name]); + $generator->line('$form->?($?);', [$this->target, $name]); + } + + /** + * {@inheritdoc} + */ + public function targetElements(): array + { + return [$this->target]; + } + + /** + * {@inheritdoc} + */ + public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface + { + return new self($method->getName()); } } diff --git a/src/Child/HttpField.php b/src/Child/HttpField.php new file mode 100644 index 0000000..0d62df6 --- /dev/null +++ b/src/Child/HttpField.php @@ -0,0 +1,78 @@ + + * $builder->float('foo')->httpFields(new ArrayOffsetHttpFields('_foo')); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[HttpField('package_length')] + * private FloatElement $packageLength; + * } + * + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * + * @see ChildBuilder::httpFields() The called method + * @see ArrayOffsetHttpFields The used HTTP fields implementation + * + * @api + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class HttpField implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The HTTP field name + * + * @var non-empty-string + * @readonly + */ + private string $name, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + if (!$builder instanceof ChildBuilder) { + throw new LogicException('The HttpField attribute can only be used on a ChildBuilder instance'); + } + + $builder->httpFields(new ArrayOffsetHttpFields($this->name)); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->use(ArrayOffsetHttpFields::class); + $generator->line('$?->httpFields(new ArrayOffsetHttpFields(?));', [$name, $this->name]); + } +} diff --git a/src/Constraint/AsConstraint.php b/src/Constraint/AsConstraint.php new file mode 100644 index 0000000..ef05d98 --- /dev/null +++ b/src/Constraint/AsConstraint.php @@ -0,0 +1,91 @@ + + * $builder->integer('foo')->satisfy([$this, 'validateFoo'], 'Foo is invalid'); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[AsConstraint('foo', message: 'Foo is invalid')] + * public function validateFoo($value, ElementInterface $input): bool + * { + * return $value % 5 > 2; + * } + * } + * + * + * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * + * @see ElementBuilderInterface::satisfy() The called method + * @see Constraint + * @see Closure The used constraint + * @see CallbackConstraint For annotate the property instead of the method + * @see AsArrayConstraint For use a constraint on an array element + * + * @api + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +final class AsConstraint implements MethodChildBuilderAttributeInterface +{ + public function __construct( + /** + * The element property name to which the constraint is applied + * + * @var literal-string + * @readonly + */ + private string $target, + /** + * The error message to use + * This option is used only if the validator return false, in other cases, + * the message returned by the validator will be used + * + * @var string|null + * @readonly + */ + private ?string $message = null, + ) { + } + + /** + * {@inheritdoc} + */ + public function targetElements(): array + { + return [$this->target]; + } + + /** + * {@inheritdoc} + */ + public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface + { + return new CallbackConstraint($method->getName(), $this->message); + } +} diff --git a/src/Constraint/CallbackConstraint.php b/src/Constraint/CallbackConstraint.php index e28ee96..249bea6 100644 --- a/src/Constraint/CallbackConstraint.php +++ b/src/Constraint/CallbackConstraint.php @@ -42,6 +42,7 @@ * @see ElementBuilderInterface::satisfy() The called method * @see Constraint * @see Closure The used constraint + * @see AsConstraint To annotate the method instead of the property * * @api */ @@ -61,7 +62,7 @@ public function __construct( * - Return string for a custom error message * - Return array with error message and code * - * @var literal-string + * @var non-empty-string * @readonly */ private string $methodName, diff --git a/src/Constraint/Satisfy.php b/src/Constraint/Satisfy.php index fa046d4..83c7ab6 100644 --- a/src/Constraint/Satisfy.php +++ b/src/Constraint/Satisfy.php @@ -6,11 +6,15 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; -use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; +use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation; use Bdf\Form\Child\ChildBuilderInterface; +use InvalidArgumentException; use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; +use function is_object; +use function is_string; + /** * Define a custom constraint for an element, using a validation method * @@ -27,6 +31,10 @@ * { * #[Satisfy(MyConstraint::class, ['foo' => 'bar'])] * private IntegerElement $foo; + * + * // or on PHP 8.1 + * #[Satisfy(new MyConstraint(['foo' => 'bar']))] + * private IntegerElement $foo; * } * * @@ -42,12 +50,18 @@ class Satisfy implements ChildBuilderAttributeInterface { public function __construct( /** - * The constraint class name + * The constraint + * + * You can use a class name, and provider arguments on the next parameter, + * or directly use the constraint instance. * - * @var class-string + * When a constraint instance is used, in case of code generation, + * the constructor parameters will be deduced from public properties of the constraint. + * This may not work if the constraint has a complex constructor. + * @var class-string|Constraint * @readonly */ - private string $constraint, + private string|Constraint $constraint, /** * Constraint's constructor options * @@ -56,6 +70,9 @@ public function __construct( */ private mixed $options = null ) { + if (is_object($constraint) && $options !== null) { + throw new InvalidArgumentException('Cannot use options with constraint instance'); + } } /** @@ -71,7 +88,12 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - $type = $generator->useAndSimplifyType($this->constraint); - $generator->line('$?->satisfy(?::class, ?);', [$name, new Literal($type), $this->options]); + if (is_string($this->constraint)) { + $type = $generator->useAndSimplifyType($this->constraint); + $generator->line('$?->satisfy(?::class, ?);', [$name, new Literal($type), $this->options]); + } else { + $constraint = ObjectInstantiation::singleArrayParameter($this->constraint)->render($generator); + $generator->line('$?->satisfy(?);', [$name, $constraint]); + } } } diff --git a/src/Element/AsTransformer.php b/src/Element/AsTransformer.php new file mode 100644 index 0000000..aea8c06 --- /dev/null +++ b/src/Element/AsTransformer.php @@ -0,0 +1,82 @@ + + * $builder->string('foo')->transformer([$this, 'myTransformer']); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * private IntegerElement $bar; + * + * #[AsTransformer('bar')] + * public function barTransformer($value, IntegerElement $input, bool $toPhp) + * { + * return $toPhp ? hexdec($value) : dechex($value); + * } + * } + * + * + * @implements MethodChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> + * + * @see ElementBuilderInterface::transformer() The called method + * @see Transformer For use a transformer class as transformer + * @see AsModelTransformer For use transformer in same way, but for model transformer intead of http one + * @see CallbackTransformer To annotate the property instead + * + * @api + */ +#[Attribute(Attribute::TARGET_METHOD)] +final class AsTransformer implements MethodChildBuilderAttributeInterface +{ + /** + * @var list + * @readonly + */ + private array $targets; + + /** + * @param non-empty-string ...$targets Target elements properties names + * @no-named-arguments + */ + public function __construct(string ...$targets) + { + $this->targets = $targets; + } + + /** + * {@inheritdoc} + */ + public function targetElements(): array + { + return $this->targets; + } + + /** + * {@inheritdoc} + */ + public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface + { + return new CallbackTransformer($method->getName()); + } +} diff --git a/src/Element/CallbackTransformer.php b/src/Element/CallbackTransformer.php index 330dfe2..310b166 100644 --- a/src/Element/CallbackTransformer.php +++ b/src/Element/CallbackTransformer.php @@ -84,21 +84,21 @@ public function __construct( * Method name use to define the unified transformer method * If defined, the other parameters will be ignored * - * @var literal-string|null + * @var non-empty-string|null * @readonly */ private ?string $callback = null, /** * Method name use to define the transformation process from http value to the input * - * @var literal-string|null + * @var non-empty-string|null * @readonly */ private ?string $fromHttp = null, /** * Method name use to define the transformation process from input value to http format * - * @var literal-string|null + * @var non-empty-string|null * @readonly */ private ?string $toHttp = null, diff --git a/src/Element/Date/AfterField.php b/src/Element/Date/AfterField.php new file mode 100644 index 0000000..b231d6a --- /dev/null +++ b/src/Element/Date/AfterField.php @@ -0,0 +1,81 @@ + + * $builder->dateTime('dateEnd')->afterField('dateStart'); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * private DateTimeElement $dateStart; + * + * #[Dependencies('dateStart'), AfterField('dateStart')] + * private DateTimeElement $dateEnd; + * } + * + * + * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::afterField() The called method + * @see BeforeField For the opposite constraint + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + * + * @api + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class AfterField implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The field name to compare + * + * @var non-empty-string + * @readonly + */ + private string $field, + /** + * The error message. + * If not set, the default message will be used + * + * @var string|null + */ + private ?string $message = null, + /** + * If true, will allow the date to be equal to the other field + * + * @var bool + */ + private bool $orEqual = false, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->afterField($this->field, $this->message, $this->orEqual); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->afterField(?, ?, ?);', [$name, $this->field, $this->message, $this->orEqual]); + } +} diff --git a/src/Element/Date/BeforeField.php b/src/Element/Date/BeforeField.php new file mode 100644 index 0000000..504e059 --- /dev/null +++ b/src/Element/Date/BeforeField.php @@ -0,0 +1,81 @@ + + * $builder->dateTime('dateEnd')->beforeField('dateStart'); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * private DateTimeElement $dateStart; + * + * #[Dependencies('dateEnd'), BeforeField('dateEnd')] + * private DateTimeElement $dateStart; + * } + * + * + * @see \Bdf\Form\Leaf\Date\DateTimeElementBuilder::beforeField() The called method + * @see AfterField For the opposite constraint + * + * @implements ChildBuilderAttributeInterface<\Bdf\Form\Leaf\Date\DateTimeElementBuilder> + * + * @api + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class BeforeField implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * The field name to compare + * + * @var non-empty-string + * @readonly + */ + private string $field, + /** + * The error message. + * If not set, the default message will be used + * + * @var string|null + */ + private ?string $message = null, + /** + * If true, will allow the date to be equal to the other field + * + * @var bool + */ + private bool $orEqual = false, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->beforeField($this->field, $this->message, $this->orEqual); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->beforeField(?, ?, ?);', [$name, $this->field, $this->message, $this->orEqual]); + } +} diff --git a/src/Element/Required.php b/src/Element/Required.php new file mode 100644 index 0000000..4f2629a --- /dev/null +++ b/src/Element/Required.php @@ -0,0 +1,66 @@ + + * $builder->float('foo')->required(); + * + * + * Usage: + * + * class MyForm extends AttributeForm + * { + * #[Required] + * private IntegerElement $foo; + * } + * + * + * @see AbstractElementBuilder::required() The called method + * + * @implements ChildBuilderAttributeInterface + * + * @api + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class Required implements ChildBuilderAttributeInterface +{ + public function __construct( + /** + * Define the message to display if the element is not filled + * If not set, the default message will be used + * + * @readonly + */ + private ?string $message = null, + ) { + } + + /** + * {@inheritdoc} + */ + public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void + { + $builder->required($this->message); + } + + /** + * {@inheritdoc} + */ + public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void + { + $generator->line('$?->required(?);', [$name, $this->message]); + } +} diff --git a/src/Element/Transformer.php b/src/Element/Transformer.php index 85a7aed..bf90a96 100644 --- a/src/Element/Transformer.php +++ b/src/Element/Transformer.php @@ -3,6 +3,8 @@ namespace Bdf\Form\Attribute\Element; use Attribute; +use Bdf\Form\Aggregate\ArrayElementBuilder; +use Bdf\Form\Attribute\Aggregate\ArrayTransformer; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; @@ -34,6 +36,7 @@ * @implements ChildBuilderAttributeInterface<\Bdf\Form\ElementBuilderInterface> * * @see ElementBuilderInterface::transformer() The called method + * @see ArrayElementBuilder::arrayTransformer() The called method if array flag is set * @see CallbackTransformer For use custom methods as transformer instead of class * * @api @@ -56,6 +59,20 @@ public function __construct( * @readonly */ private array $constructorArguments = [], + /** + * Apply the transformer on the whole array element + * instead of each element + * + * If set to true, {@see ArrayElementBuilder::arrayTransformer()} will be used + * + * Note: this flag can be used only on array element + * + * @var bool + * @readonly + * + * @see ArrayTransformer Prefer use this attribute for array element, instead of manually set this flag + */ + private bool $array = false, ) { } @@ -64,7 +81,14 @@ public function __construct( */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - $builder->transformer(new $this->transformerClass(...$this->constructorArguments)); + $transformer = new $this->transformerClass(...$this->constructorArguments); + + if ($this->array) { + /** @var ChildBuilderInterface $builder */ + $builder->arrayTransformer($transformer); + } else { + $builder->transformer($transformer); + } } /** @@ -73,6 +97,8 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $transformer = $generator->useAndSimplifyType($this->transformerClass); - $generator->line('$?->transformer(new ?(...?));', [$name, new Literal($transformer), $this->constructorArguments]); + $code = $this->array ? '$?->arrayTransformer(new ?(...?));' : '$?->transformer(new ?(...?));'; + + $generator->line($code, [$name, new Literal($transformer), $this->constructorArguments]); } } diff --git a/src/Processor/CodeGenerator/ObjectInstantiation.php b/src/Processor/CodeGenerator/ObjectInstantiation.php new file mode 100644 index 0000000..9d1630c --- /dev/null +++ b/src/Processor/CodeGenerator/ObjectInstantiation.php @@ -0,0 +1,92 @@ +useAndSimplifyType($this->className) : $this->className; + + return Literal::new($className, $this->constructorParameters); + } + + /** + * Configure the ObjectInstantiation utility to generate + * the constructor call with a single array parameter, + * use to inject all public properties of an object. + * + * This method should be used for symfony constraints. + * Properties with default value will be ignored. + * + * @param object $object + * @return self + */ + public static function singleArrayParameter(object $object): self + { + return new self( + get_class($object), + [self::extractPublicProperties($object)] + ); + } + + /** + * Extract all public properties of the object, ignoring default values + * + * @param object $object + * + * @return array + * @psalm-suppress MixedAssignment + */ + private static function extractPublicProperties(object $object): array + { + $reflectionObject = new ReflectionObject($object); + $properties = []; + + foreach ($reflectionObject->getProperties() as $property) { + if ($property->isPublic() && !$property->isStatic()) { + $property->setAccessible(true); + $value = $property->getValue($object); + + if ($property->getDefaultValue() !== $value) { + $properties[$property->getName()] = $value; + } + } + } + + return $properties; + } +} diff --git a/src/Processor/ConfigureFormBuilderStrategy.php b/src/Processor/ConfigureFormBuilderStrategy.php index cd081fa..62aece7 100644 --- a/src/Processor/ConfigureFormBuilderStrategy.php +++ b/src/Processor/ConfigureFormBuilderStrategy.php @@ -39,7 +39,7 @@ public function __construct() /** * {@inheritdoc} */ - public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void + public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { $attribute->newInstance()->applyOnFormBuilder($form, $builder); @@ -49,7 +49,7 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For /** * {@inheritdoc} */ - public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void + public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $submitBuilder = $builder->submit($name); @@ -61,7 +61,7 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att /** * {@inheritdoc} */ - public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void + public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $elementBuilder = $builder->add($name, $elementType); @@ -80,14 +80,18 @@ public function onElementProperty(ReflectionProperty $property, string $name, st } } } + + foreach ($metadata->registeredChildAttributes($name) as $attribute) { + $attribute->applyOnChildBuilder($form, $elementBuilder); + } } /** * {@inheritdoc} */ - public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface + public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface { - return new PostConfigureReflectionSetProperties($elementProperties, $buttonProperties); + return new PostConfigureReflectionSetProperties($metadata->elementProperties(), $metadata->buttonProperties()); } /** diff --git a/src/Processor/GenerateConfiguratorStrategy.php b/src/Processor/GenerateConfiguratorStrategy.php index 5e2ae5f..503a884 100644 --- a/src/Processor/GenerateConfiguratorStrategy.php +++ b/src/Processor/GenerateConfiguratorStrategy.php @@ -50,19 +50,24 @@ public function __construct(string $className) /** * {@inheritdoc} */ - public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void + public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { + $empty = true; + foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { $attribute->newInstance()->generateCodeForFormBuilder($this->generator, $form); + $empty = false; } - $this->generator->line(); + if (!$empty) { + $this->generator->line(); + } } /** * {@inheritdoc} */ - public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void + public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $this->generator->line('$builder->submit(?)', [$name]); @@ -76,7 +81,7 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att /** * {@inheritdoc} */ - public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void + public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $elementType = $this->generator->useAndSimplifyType($elementType); $this->generator->line('$? = $builder->add(?, ?::class);', [$name, $name, new Literal($elementType)]); @@ -96,13 +101,17 @@ public function onElementProperty(ReflectionProperty $property, string $name, st } } + foreach ($metadata->registeredChildAttributes($name) as $attribute) { + $attribute->generateCodeForChildBuilder($name, $this->generator, $form); + } + $this->generator->line(); // Add empty line } /** * {@inheritdoc} */ - public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface + public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface { $this->generator->line('return $this;'); @@ -111,6 +120,9 @@ public function onPostConfigure(array $elementProperties, array $buttonPropertie ->implementsMethod(PostConfigureInterface::class, 'postConfigure') ; + $elementProperties = $metadata->elementProperties(); + $buttonProperties = $metadata->buttonProperties(); + if (!empty($buttonProperties)) { $method->addBody('$root = $form->root();'); } diff --git a/src/Processor/MethodChildBuilderAttributeInterface.php b/src/Processor/MethodChildBuilderAttributeInterface.php new file mode 100644 index 0000000..1e15a40 --- /dev/null +++ b/src/Processor/MethodChildBuilderAttributeInterface.php @@ -0,0 +1,33 @@ + + */ + public function targetElements(): array; + + /** + * The actual object which will be applied on the child builder + * + * @param ReflectionMethod $method The method where the attribute is located + * @return ChildBuilderAttributeInterface + */ + public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface; +} diff --git a/src/Processor/ProcessorMetadata.php b/src/Processor/ProcessorMetadata.php new file mode 100644 index 0000000..4ccf720 --- /dev/null +++ b/src/Processor/ProcessorMetadata.php @@ -0,0 +1,93 @@ + + */ + private array $buttonProperties = []; + + /** + * @var array + */ + private array $elementProperties = []; + + /** + * @var array> + */ + private array $childAttributes = []; + + /** + * @param non-empty-string $name + * @param ReflectionProperty $property + * @return void + */ + public function addButtonProperty(string $name, ReflectionProperty $property): void + { + $this->buttonProperties[$name] = $property; + } + + /** + * @param non-empty-string $name + * @param ReflectionProperty $property + * @return void + */ + public function addElementProperty(string $name, ReflectionProperty $property): void + { + $this->elementProperties[$name] = $property; + } + + public function addChildAttribute(string $elementName, ChildBuilderAttributeInterface $attribute): void + { + $this->childAttributes[$elementName][] = $attribute; + } + + /** + * @return array + */ + public function buttonProperties(): array + { + return $this->buttonProperties; + } + + /** + * @return array + */ + public function elementProperties(): array + { + return $this->elementProperties; + } + + /** + * Check if the given property has already been registered + * + * @param string $name The property name + * @return bool + */ + public function hasProperty(string $name): bool + { + return isset($this->buttonProperties[$name]) || isset($this->elementProperties[$name]); + } + + /** + * Get child attributes manually registered for the given element name + * Those attributes are generally registered by the {@see MethodChildBuilderAttributeInterface} attributes on methods + * + * @param string $name + * @return list + */ + public function registeredChildAttributes(string $name): array + { + return $this->childAttributes[$name] ?? []; + } +} diff --git a/src/Processor/ReflectionProcessor.php b/src/Processor/ReflectionProcessor.php index e620d80..2dd97a2 100644 --- a/src/Processor/ReflectionProcessor.php +++ b/src/Processor/ReflectionProcessor.php @@ -6,7 +6,9 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Button\ButtonInterface; use Bdf\Form\ElementInterface; +use ReflectionAttribute; use ReflectionClass; +use ReflectionMethod; use ReflectionNamedType; /** @@ -39,11 +41,13 @@ public function __construct(ReflectionStrategyInterface $strategy) */ public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { - $elementProperties = []; - $buttonProperties = []; + $metadata = new ProcessorMetadata(); + + // First iterate over methods to build the metadata + $this->registerMethodsMetadata($form, $metadata); foreach ($this->iterateClassHierarchy($form) as $formClass) { - $this->strategy->onFormClass($formClass, $form, $builder); + $this->strategy->onFormClass($formClass, $form, $builder, $metadata); foreach ($formClass->getProperties() as $property) { $name = $property->getName(); @@ -51,8 +55,7 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil if ( !$property->hasType() || !$property->getType() instanceof ReflectionNamedType - || isset($elementProperties[$name]) - || isset($buttonProperties[$name]) + || $metadata->hasProperty($name) ) { continue; } @@ -61,16 +64,16 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil $property->setAccessible(true); if ($elementType === ButtonInterface::class) { - $buttonProperties[$name] = $property; - $this->strategy->onButtonProperty($property, $name, $form, $builder); + $metadata->addButtonProperty($name, $property); + $this->strategy->onButtonProperty($property, $name, $form, $builder, $metadata); } elseif (is_subclass_of($elementType, ElementInterface::class)) { - $elementProperties[$name] = $property; - $this->strategy->onElementProperty($property, $name, $elementType, $form, $builder); + $metadata->addElementProperty($name, $property); + $this->strategy->onElementProperty($property, $name, $elementType, $form, $builder, $metadata); } } } - return $this->strategy->onPostConfigure($elementProperties, $buttonProperties, $form); + return $this->strategy->onPostConfigure($metadata, $form); } /** @@ -89,4 +92,26 @@ private function iterateClassHierarchy(AttributeForm $form): iterable yield $reflection; } } + + /** + * Fill the metadata from methods attributes + * + * @param AttributeForm $form + * @param ProcessorMetadata $metadata + * + * @return void + */ + private function registerMethodsMetadata(AttributeForm $form, ProcessorMetadata $metadata): void + { + foreach ((new ReflectionClass($form))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + foreach ($method->getAttributes(MethodChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + /** @var MethodChildBuilderAttributeInterface $attributeInstance */ + $attributeInstance = $attribute->newInstance(); + + foreach ($attributeInstance->targetElements() as $target) { + $metadata->addChildAttribute($target, $attributeInstance->attribute($method)); + } + } + } + } } diff --git a/src/Processor/ReflectionStrategyInterface.php b/src/Processor/ReflectionStrategyInterface.php index 719ae0b..dd72a91 100644 --- a/src/Processor/ReflectionStrategyInterface.php +++ b/src/Processor/ReflectionStrategyInterface.php @@ -25,10 +25,11 @@ interface ReflectionStrategyInterface * @param ReflectionClass $formClass Form class to use * @param AttributeForm $form The current form instance * @param FormBuilderInterface $builder Builder to configure + * @param ProcessorMetadata $metadata Metadata for the current form * * @return void */ - public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder): void; + public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void; /** * Configure a button following the declared property @@ -39,10 +40,11 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For * @param non-empty-string $name The button name * @param AttributeForm $form The current form instance * @param FormBuilderInterface $builder Builder to configure + * @param ProcessorMetadata $metadata Metadata for the current form * * @return void */ - public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder): void; + public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void; /** * Configure an element following the declared property @@ -54,16 +56,17 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att * @param class-string $elementType The element type (i.e. the property type) * @param AttributeForm $form The current form instance * @param FormBuilderInterface $builder Builder to configure + * @param ProcessorMetadata $metadata Metadata for the current form * * @return void */ - public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder): void; + public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void; /** - * @param array $elementProperties - * @param array $buttonProperties - * @param AttributeForm $form + * @param ProcessorMetadata $metadata Metadata for the current form + * @param AttributeForm $form The current form instance + * * @return PostConfigureInterface|null */ - public function onPostConfigure(array $elementProperties, array $buttonProperties, AttributeForm $form): ?PostConfigureInterface; + public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface; } diff --git a/tests/Aggregate/ArrayTransformerTest.php b/tests/Aggregate/ArrayTransformerTest.php new file mode 100644 index 0000000..0f7c0d6 --- /dev/null +++ b/tests/Aggregate/ArrayTransformerTest.php @@ -0,0 +1,94 @@ +submit(['foo' => ['_', '-']]); + $this->assertEquals(['A_', 'A-'], $form->foo->value()); + + $view = $form->view(); + $this->assertEquals(['A_A', 'A-A'], $view['foo']->value()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[ArrayTransformer(AArrayTransformer::class, ['A'])] + public ArrayElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Tests\Form\Attribute\Aggregate\AArrayTransformer; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', ArrayElement::class); + $foo->arrayTransformer(new AArrayTransformer('A')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } +} + +class AArrayTransformer implements TransformerInterface +{ + public function __construct( + public string $c + ) { + } + + public function transformToHttp($value, ElementInterface $input) + { + return array_map(fn($v) => $v . $this->c, $value); + } + + public function transformFromHttp($value, ElementInterface $input) + { + return array_map(fn($v) => $this->c . $v, $value); + } +} diff --git a/tests/Aggregate/AsArrayConstraintTest.php b/tests/Aggregate/AsArrayConstraintTest.php new file mode 100644 index 0000000..baaae23 --- /dev/null +++ b/tests/Aggregate/AsArrayConstraintTest.php @@ -0,0 +1,108 @@ +submit(['foo' => ['a']]); + + $this->assertFalse($form->valid()); + $this->assertEquals('Foo size must be a multiple of 2', $form->foo->error()->global()); + + $form->submit(['foo' => ['a', 'b']]); + + $this->assertTrue($form->valid()); + $this->assertNull($form->foo->error()->global()); + + $form->submit(['bar' => ['a']]); + + $this->assertFalse($form->valid()); + $this->assertEquals('The value is invalid', $form->bar->error()->global()); + + $form->submit(['bar' => ['a', 'b']]); + + $this->assertTrue($form->valid()); + $this->assertNull($form->bar->error()->global()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + public ArrayElement $foo; + + public ArrayElement $bar; + + #[AsArrayConstraint('foo', message: 'Foo size must be a multiple of 2')] + #[AsArrayConstraint('bar')] + public function validateFoo(array $value): bool + { + return count($value) % 2 === 0; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Constraint\Closure as ClosureConstraint; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', ArrayElement::class); + $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2'])); + + $bar = $builder->add('bar', ArrayElement::class); + $bar->arrayConstraint(new ClosureConstraint([$form, 'validateFoo'])); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Child/AsFilterTest.php b/tests/Child/AsFilterTest.php new file mode 100644 index 0000000..4471fad --- /dev/null +++ b/tests/Child/AsFilterTest.php @@ -0,0 +1,98 @@ +submit(['a' => 'Zm9v']); + $this->assertEquals('foo', $form->a->value()); + } + + /** + * + */ + public function test_code_generator() + { + $form = new class() extends AttributeForm { + #[Getter, Setter] + public StringElement $foo; + public StringElement $bar; + + #[AsFilter('foo', 'bar')] + public function aFilter($value, Child $input, $default) + { + return base64_decode($value); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->extractor(new Getter()); + $foo->hydrator(new Setter()); + $foo->filter([$form, 'aFilter']); + + $bar = $builder->add('bar', StringElement::class); + $bar->filter([$form, 'aFilter']); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form +); + } +} diff --git a/tests/Child/AsModelTransformerTest.php b/tests/Child/AsModelTransformerTest.php new file mode 100644 index 0000000..53a292d --- /dev/null +++ b/tests/Child/AsModelTransformerTest.php @@ -0,0 +1,96 @@ +submit(['a' => 'foo']); + $this->assertEquals(new Struct(a: 'Zm9v'), $form->value()); + + $form->import(new Struct(a: 'SGVsbG8gV29ybGQgIQ==')); + $this->assertEquals('Hello World !', $form->a->value()); + } + + /** + * + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Getter, Setter] + public IntegerElement $foo; + + #[AsModelTransformer('foo')] + public function t($value, $input) + { + return $value + 1; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', IntegerElement::class); + $foo->extractor(new Getter()); + $foo->hydrator(new Setter()); + $foo->modelTransformer([$form, 't']); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } +} diff --git a/tests/Child/CallbackFilterTest.php b/tests/Child/CallbackFilterTest.php new file mode 100644 index 0000000..fe82523 --- /dev/null +++ b/tests/Child/CallbackFilterTest.php @@ -0,0 +1,90 @@ +submit(['a' => 'Zm9v']); + $this->assertEquals('foo', $form->a->value()); + } + + /** + * + */ + public function test_code_generator() + { + $form = new class() extends AttributeForm { + #[CallbackFilter('aFilter'), Getter, Setter] + public StringElement $foo; + + public function aFilter($value, Child $input, $default) + { + return base64_decode($value); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->filter([$form, 'aFilter']); + $foo->extractor(new Getter()); + $foo->hydrator(new Setter()); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } +} diff --git a/tests/Child/ConfigureTest.php b/tests/Child/ConfigureTest.php index a557b54..aea2064 100644 --- a/tests/Child/ConfigureTest.php +++ b/tests/Child/ConfigureTest.php @@ -38,6 +38,32 @@ public function configureFoo(ChildBuilderInterface $builder): void $this->assertTrue($form->valid()); } + /** + * @dataProvider provideAttributesProcessor + */ + public function test_on_method(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { + public StringElement $foo; + + /** + * @param ChildBuilderInterface|StringElementBuilder $builder + */ + #[Configure('foo')] + public function configureFoo(ChildBuilderInterface $builder): void + { + $builder->length(['min' => 3]); + } + }; + + $form->submit(['foo' => 'a']); + $this->assertFalse($form->valid()); + $this->assertEquals(['foo' => 'This value is too short. It should have 3 characters or more.'], $form->error()->toArray()); + + $form->submit(['foo' => 'abc']); + $this->assertTrue($form->valid()); + } + public function test_code_generator() { $form = new class extends AttributeForm { @@ -85,6 +111,58 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void } } +PHP + , $form +); + } + + public function test_code_generator_on_method() + { + $form = new class extends AttributeForm { + public StringElement $foo; + + /** + * @param ChildBuilderInterface|StringElementBuilder $builder + */ + #[Configure('foo')] + public function configureFoo(ChildBuilderInterface $builder): void + { + $builder->length(['min' => 3]); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $form->configureFoo($foo); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + PHP , $form ); diff --git a/tests/Child/HttpFieldTest.php b/tests/Child/HttpFieldTest.php new file mode 100644 index 0000000..41ffe08 --- /dev/null +++ b/tests/Child/HttpFieldTest.php @@ -0,0 +1,82 @@ +submit(['_v_' => 42]); + $this->assertSame(42, $form->v->value()); + $this->assertSame(['_v_' => '42'], $form->httpValue()); + } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[HttpField('_v_')] + public IntegerElement $v; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Child\Http\ArrayOffsetHttpFields; +use Bdf\Form\Leaf\IntegerElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $v = $builder->add('v', IntegerElement::class); + $v->httpFields(new ArrayOffsetHttpFields('_v_')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->v = $inner['v']->element(); + } +} + +PHP + , $form); + } +} diff --git a/tests/Constraint/AsConstraintTest.php b/tests/Constraint/AsConstraintTest.php new file mode 100644 index 0000000..6677a39 --- /dev/null +++ b/tests/Constraint/AsConstraintTest.php @@ -0,0 +1,100 @@ +submit(['foo' => 'a']); + + $this->assertFalse($form->valid()); + $this->assertEquals('Foo length must be a multiple of 2', $form->foo->error()->global()); + + $form->submit(['foo' => 'abcd']); + + $this->assertTrue($form->valid()); + $this->assertNull($form->foo->error()->global()); + + $form->submit(['bar' => 'a']); + + $this->assertFalse($form->valid()); + $this->assertEquals('The value is invalid', $form->bar->error()->global()); + + $form->submit(['bar' => 'abcd']); + + $this->assertTrue($form->valid()); + $this->assertNull($form->bar->error()->global()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + public StringElement $foo; + + #[AsConstraint('foo', message: 'Foo length must be a multiple of 2')] + public function validateFoo($value): bool + { + return strlen($value) % 2 === 0; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Constraint\Closure as ClosureConstraint; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->satisfy(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo length must be a multiple of 2'])); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } +} diff --git a/tests/Element/AsTransformerTest.php b/tests/Element/AsTransformerTest.php new file mode 100644 index 0000000..c6161bf --- /dev/null +++ b/tests/Element/AsTransformerTest.php @@ -0,0 +1,85 @@ +submit(['foo' => 'a']); + + $this->assertEquals('["a",true]', $form->foo->value()); + + $view = $form->view(); + + $this->assertEquals('["[\"a\",true]",false]', $view['foo']->value()); + } + + + public function test_code_generator() + { + $form = new class() extends AttributeForm { + public StringElement $foo; + + #[AsTransformer('foo')] + public function fooTransformer($value, StringElement $input, bool $toPhp) + { + return json_encode([$value, $toPhp]); + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->transformer([$form, 'fooTransformer']); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form); + } +} diff --git a/tests/Element/Date/AfterFieldTest.php b/tests/Element/Date/AfterFieldTest.php new file mode 100644 index 0000000..34e6be7 --- /dev/null +++ b/tests/Element/Date/AfterFieldTest.php @@ -0,0 +1,158 @@ +submit([ + 'foo' => '2020-11-02T15:23:00Z', + 'bar' => '2020-11-02T15:21:00Z', + ]); + + $this->assertTrue($form->valid()); + + $form->submit([ + 'foo' => '2020-11-02T15:20:50Z', + 'bar' => '2020-11-02T15:21:50Z', + ]); + $this->assertFalse($form->valid()); + $this->assertEquals([ + 'foo' => 'This value should be greater than Nov 2, 2020, 3:21 PM.', + ], self::normalizeSpace($form->error()->toArray())); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_with_message(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { + #[Dependencies('bar'), AfterField('bar', message: 'my error')] + public DateTimeElement $foo; + + public DateTimeElement $bar; + }; + + $form->submit([ + 'foo' => '2020-11-02T15:23:00Z', + 'bar' => '2020-11-02T15:21:00Z', + ]); + + $this->assertTrue($form->valid()); + + $form->submit([ + 'foo' => '2020-11-02T15:20:50Z', + 'bar' => '2020-11-02T15:21:00Z', + ]); + $this->assertFalse($form->valid()); + $this->assertEquals([ + 'foo' => 'my error', + ], $form->error()->toArray()); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_with_or_equal(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { + #[Dependencies('bar'), AfterField('bar', orEqual: true)] + public DateTimeElement $foo; + + public DateTimeElement $bar; + }; + + $form->submit([ + 'foo' => '2020-11-02T15:21:00Z', + 'bar' => '2020-11-02T15:21:00Z', + ]); + + $this->assertTrue($form->valid()); + + $form->submit([ + 'foo' => '2020-11-02T15:20:50Z', + 'bar' => '2020-11-02T15:21:00Z', + ]); + $this->assertFalse($form->valid()); + $this->assertEquals([ + 'foo' => 'This value should be greater than or equal to Nov 2, 2020, 3:21 PM.', + ], self::normalizeSpace($form->error()->toArray())); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Dependencies('bar'), AfterField('bar', 'my error', true)] + public DateTimeElement $foo; + public DateTimeElement $bar; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\Date\DateTimeElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', DateTimeElement::class); + $foo->depends('bar'); + $foo->afterField('bar', 'my error', true); + + $bar = $builder->add('bar', DateTimeElement::class); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form + ); + } + + public static function normalizeSpace(array|string $value): array|string + { + if (\is_array($value)) { + return \array_map([self::class, 'normalizeSpace'], $value); + } + + return \preg_replace('/\p{Zs}+/u', ' ', $value); + } +} diff --git a/tests/Element/Date/BeforeFieldTest.php b/tests/Element/Date/BeforeFieldTest.php new file mode 100644 index 0000000..180ba86 --- /dev/null +++ b/tests/Element/Date/BeforeFieldTest.php @@ -0,0 +1,159 @@ +submit([ + 'foo' => '2020-11-02T15:21:00Z', + 'bar' => '2020-11-02T15:23:00Z', + ]); + + $this->assertTrue($form->valid()); + + $form->submit([ + 'foo' => '2020-11-02T15:21:00Z', + 'bar' => '2020-11-02T15:20:50Z', + ]); + $this->assertFalse($form->valid()); + $this->assertEquals([ + 'foo' => 'This value should be less than Nov 2, 2020, 3:20 PM.', + ], self::normalizeSpace($form->error()->toArray())); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_with_message(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { + #[Dependencies('bar'), BeforeField('bar', message: 'my error')] + public DateTimeElement $foo; + + public DateTimeElement $bar; + }; + + $form->submit([ + 'foo' => '2020-11-02T15:21:00Z', + 'bar' => '2020-11-02T15:23:00Z', + ]); + + $this->assertTrue($form->valid()); + + $form->submit([ + 'foo' => '2020-11-02T15:21:00Z', + 'bar' => '2020-11-02T15:20:50Z', + ]); + $this->assertFalse($form->valid()); + $this->assertEquals([ + 'foo' => 'my error', + ], $form->error()->toArray()); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_with_or_equal(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { + #[Dependencies('bar'), BeforeField('bar', orEqual: true)] + public DateTimeElement $foo; + + public DateTimeElement $bar; + }; + + $form->submit([ + 'foo' => '2020-11-02T15:21:00Z', + 'bar' => '2020-11-02T15:21:00Z', + ]); + + $this->assertTrue($form->valid()); + + $form->submit([ + 'foo' => '2020-11-02T15:21:00Z', + 'bar' => '2020-11-02T15:20:50Z', + ]); + $this->assertFalse($form->valid()); + $this->assertEquals([ + 'foo' => 'This value should be less than or equal to Nov 2, 2020, 3:20 PM.', + ], self::normalizeSpace($form->error()->toArray())); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Dependencies('bar'), BeforeField('bar', 'my error', true)] + public DateTimeElement $foo; + public DateTimeElement $bar; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\Date\DateTimeElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', DateTimeElement::class); + $foo->depends('bar'); + $foo->beforeField('bar', 'my error', true); + + $bar = $builder->add('bar', DateTimeElement::class); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form + ); + } + + public static function normalizeSpace(array|string $value): array|string + { + if (\is_array($value)) { + return \array_map([self::class, 'normalizeSpace'], $value); + } + + return \preg_replace('/\p{Zs}+/u', ' ', $value); + } +} diff --git a/tests/Element/RequiredTest.php b/tests/Element/RequiredTest.php new file mode 100644 index 0000000..6df30e7 --- /dev/null +++ b/tests/Element/RequiredTest.php @@ -0,0 +1,84 @@ +submit(['foo' => '']); + $this->assertEquals([ + 'foo' => 'This value should not be blank.', + 'bar' => 'my message', + ], $form->error()->toArray()); + + $form->submit(['foo' => '1.2', 'bar' => '4.5']); + $this->assertTrue($form->valid()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Required] + public FloatElement $foo; + #[Required('my message')] + public FloatElement $bar; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\FloatElement; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', FloatElement::class); + $foo->required(null); + + $bar = $builder->add('bar', FloatElement::class); + $bar->required('my message'); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Element/TransformerTest.php b/tests/Element/TransformerTest.php index 6da284c..8f24d2e 100644 --- a/tests/Element/TransformerTest.php +++ b/tests/Element/TransformerTest.php @@ -2,6 +2,7 @@ namespace Tests\Form\Attribute\Element; +use Bdf\Form\Aggregate\ArrayElement; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Element\Transformer; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; @@ -29,6 +30,23 @@ public function test(AttributesProcessorInterface $processor) $this->assertEquals('A_A', $view['foo']->value()); } + /** + * @dataProvider provideAttributesProcessor + */ + public function testWithArray(AttributesProcessorInterface $processor) + { + $form = new class(null, $processor) extends AttributeForm { + #[Transformer(AArrayTransformer::class, ['A'], array: true)] + public ArrayElement $foo; + }; + + $form->submit(['foo' => ['_', '-']]); + $this->assertEquals(['A_', 'A-'], $form->foo->value()); + + $view = $form->view(); + $this->assertEquals(['A_A', 'A-A'], $view['foo']->value()); + } + public function test_code_generator() { $form = new class extends AttributeForm { @@ -69,6 +87,51 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void } } +PHP + , $form +); + } + + public function test_code_generator_with_array() + { + $form = new class extends AttributeForm { + #[Transformer(AArrayTransformer::class, ['A'], array: true)] + public ArrayElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Tests\Form\Attribute\Element\AArrayTransformer; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', ArrayElement::class); + $foo->arrayTransformer(new AArrayTransformer('A')); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + PHP , $form ); @@ -92,3 +155,21 @@ public function transformFromHttp($value, ElementInterface $input) return $this->c . $value; } } + +class AArrayTransformer implements TransformerInterface +{ + public function __construct( + public string $c + ) { + } + + public function transformToHttp($value, ElementInterface $input) + { + return array_map(fn($v) => $v . $this->c, $value); + } + + public function transformFromHttp($value, ElementInterface $input) + { + return array_map(fn($v) => $this->c . $v, $value); + } +} diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 987e262..e84898f 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -4,6 +4,7 @@ use Attribute; use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Element\AsTransformer; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Button\ButtonInterface; use Bdf\Form\Button\SubmitButton; @@ -150,7 +151,6 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $bar->hydrator(new Setter()); $bar->satisfy(new GreaterThan(5)); - $foo = $builder->add('foo', StringElement::class); $foo->satisfy(new NotBlank()); $foo->extractor(new Getter()); @@ -178,6 +178,70 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void ); } + /** + * + */ + public function test_inheritance_code_generator_with_method() + { + $form = new ChildFormWithMethod(); + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\IntegerElement; +use Bdf\Form\Leaf\StringElement; +use Bdf\Form\PropertyAccess\Getter; +use Bdf\Form\PropertyAccess\Setter; +use Symfony\Component\Validator\Constraints\GreaterThan; +use Symfony\Component\Validator\Constraints\NotBlank; +use Tests\Form\Attribute\BaseFormWithMethod; +use Tests\Form\Attribute\ChildFormWithMethod; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $bar = $builder->add('bar', IntegerElement::class); + $bar->satisfy(new NotBlank()); + $bar->extractor(new Getter()); + $bar->hydrator(new Setter()); + $bar->satisfy(new GreaterThan(5)); + + $foo = $builder->add('foo', StringElement::class); + $foo->extractor(new Getter()); + $foo->hydrator(new Setter()); + $foo->transformer([$form, 'transform']); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + (\Closure::bind(function () use ($inner, $form) { + $form->bar = $inner['bar']->element(); + }, null, ChildFormWithMethod::class))(); + (\Closure::bind(function () use ($inner, $form) { + $form->foo = $inner['foo']->element(); + }, null, BaseFormWithMethod::class))(); + } +} + +PHP + , $form + ); + } + /** * @dataProvider provideAttributesProcessor */ @@ -261,3 +325,21 @@ public function transformFromHttp($value, ElementInterface $input) return base64_encode($value); } } + +class BaseFormWithMethod extends AttributeForm +{ + #[Getter, Setter] + private StringElement $foo; + + #[AsTransformer('foo')] + public function transform(string $value): string + { + return str_rot13($value); + } +} + +class ChildFormWithMethod extends BaseFormWithMethod +{ + #[NotBlank, Getter, Setter, GreaterThan(5)] + private IntegerElement $bar; +} diff --git a/tests/Php81/Aggregate/ArrayConstraintTest.php b/tests/Php81/Aggregate/ArrayConstraintTest.php new file mode 100644 index 0000000..67c44c5 --- /dev/null +++ b/tests/Php81/Aggregate/ArrayConstraintTest.php @@ -0,0 +1,97 @@ + 'Not unique']))] + public ArrayElement $values; + }; + + $form->submit(['values' => ['aaa', 'aaa']]); + $this->assertFalse($form->valid()); + $this->assertEquals(['values' => 'Not unique'], $form->error()->toArray()); + + $form->submit(['values' => ['aaa', 'bbb']]); + $this->assertTrue($form->valid()); + } + + /** + * @dataProvider provideAttributesProcessor + */ + public function test_disallow_constraint_instance_with_option_arg(AttributesProcessorInterface $processor) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot use options with constraint instance'); + + $form = new class(null, $processor) extends AttributeForm { + #[ArrayConstraint(new Unique(['message' => 'Not unique']), ['foo' => 'bar'])] + public ArrayElement $values; + }; + + $form->submit(['values' => ['aaa', 'aaa']]); + } + + /** + * @return void + */ + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[ArrayConstraint(new Unique(['message' => 'Not unique']))] + public ArrayElement $values; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Symfony\Component\Validator\Constraints\Unique; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $values = $builder->add('values', ArrayElement::class); + $values->arrayConstraint(new Unique(['message' => 'Not unique', 'groups' => ['Default']])); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->values = $inner['values']->element(); + } +} + +PHP + , $form); + } +} diff --git a/tests/Php81/Aggregate/CallbackArrayConstraintTest.php b/tests/Php81/Aggregate/CallbackArrayConstraintTest.php new file mode 100644 index 0000000..7c64ffc --- /dev/null +++ b/tests/Php81/Aggregate/CallbackArrayConstraintTest.php @@ -0,0 +1,108 @@ +submit(['foo' => ['a']]); + + $this->assertFalse($form->valid()); + $this->assertEquals('Foo size must be a multiple of 2', $form->foo->error()->global()); + + $form->submit(['foo' => ['a', 'b']]); + + $this->assertTrue($form->valid()); + $this->assertNull($form->foo->error()->global()); + + $form->submit(['bar' => ['a']]); + + $this->assertFalse($form->valid()); + $this->assertEquals('The value is invalid', $form->bar->error()->global()); + + $form->submit(['bar' => ['a', 'b']]); + + $this->assertTrue($form->valid()); + $this->assertNull($form->bar->error()->global()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[CallbackArrayConstraint('validateFoo', message: 'Foo size must be a multiple of 2')] + public ArrayElement $foo; + + #[CallbackArrayConstraint('validateFoo')] + public ArrayElement $bar; + + public function validateFoo(array $value): bool + { + return count($value) % 2 === 0; + } + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\ArrayElement; +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Constraint\Closure as ClosureConstraint; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', ArrayElement::class); + $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2'])); + + $bar = $builder->add('bar', ArrayElement::class); + $bar->arrayConstraint(new ClosureConstraint([$form, 'validateFoo'])); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + $form->bar = $inner['bar']->element(); + } +} + +PHP + , $form + ); + } +} diff --git a/tests/Php81/Constraint/SatisfyTest.php b/tests/Php81/Constraint/SatisfyTest.php new file mode 100644 index 0000000..61d8011 --- /dev/null +++ b/tests/Php81/Constraint/SatisfyTest.php @@ -0,0 +1,76 @@ + 3]))] + public StringElement $foo; + }; + + $form->submit(['foo' => 'ab']); + $this->assertFalse($form->valid()); + $this->assertEquals(['foo' => 'This value is too short. It should have 3 characters or more.'], $form->error()->toArray()); + + $form->submit(['foo' => 'abc']); + $this->assertTrue($form->valid()); + } + + public function test_code_generator() + { + $form = new class extends AttributeForm { + #[Satisfy(new Length(['min' => 3]))] + public StringElement $foo; + }; + + $this->assertGenerated(<<<'PHP' +namespace Generated; + +use Bdf\Form\Aggregate\FormBuilderInterface; +use Bdf\Form\Aggregate\FormInterface; +use Bdf\Form\Attribute\AttributeForm; +use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use Bdf\Form\Attribute\Processor\PostConfigureInterface; +use Bdf\Form\Leaf\StringElement; +use Symfony\Component\Validator\Constraints\Length; + +class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface +{ + /** + * {@inheritdoc} + */ + function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface + { + $foo = $builder->add('foo', StringElement::class); + $foo->satisfy(new Length(['min' => 3, 'groups' => ['Default']])); + + return $this; + } + + /** + * {@inheritdoc} + */ + function postConfigure(AttributeForm $form, FormInterface $inner): void + { + $form->foo = $inner['foo']->element(); + } +} + +PHP + , $form +); + } +} diff --git a/tests/Processor/CodeGenerator/ObjectInstantiationTest.php b/tests/Processor/CodeGenerator/ObjectInstantiationTest.php new file mode 100644 index 0000000..8d1c6be --- /dev/null +++ b/tests/Processor/CodeGenerator/ObjectInstantiationTest.php @@ -0,0 +1,40 @@ +assertEquals("new Symfony\Component\Validator\Constraints\NotBlank(['groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render()); + + $this->assertEquals($o, eval('return ' . ObjectInstantiation::singleArrayParameter($o)->render().';')); + } + + public function test_with_parameters() + { + $o = new Range(['min' => 1, 'max' => 10]); + + $this->assertEquals("new Symfony\Component\Validator\Constraints\Range(['min' => 1, 'max' => 10, 'groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render()); + + $this->assertEquals($o, eval('return ' . ObjectInstantiation::singleArrayParameter($o)->render().';')); + } + + public function test_with_simplified_class_name() + { + $o = new Range(['min' => 1, 'max' => 10]); + $generator = new ClassGenerator(new PhpNamespace('Foo'), new ClassType('Bar')); + + $this->assertEquals("new Range(['min' => 1, 'max' => 10, 'groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render($generator)); + } +} diff --git a/tests/Processor/GenerateConfiguratorStrategyTest.php b/tests/Processor/GenerateConfiguratorStrategyTest.php index 2b6f92d..7ad7fc7 100644 --- a/tests/Processor/GenerateConfiguratorStrategyTest.php +++ b/tests/Processor/GenerateConfiguratorStrategyTest.php @@ -208,13 +208,11 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? { $d = $builder->add('d', StringElement::class); - $builder->submit('b') ; $c = $builder->add('c', StringElement::class); - $a = $builder->add('a', StringElement::class); return $this; From 70845c1a2d72dfce7add23973cdb3b5cfb542eca Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 26 Jul 2024 10:16:06 +0200 Subject: [PATCH 28/40] ci: fix psalm annotations --- src/Aggregate/AsArrayConstraint.php | 2 +- src/Aggregate/CallbackArrayConstraint.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aggregate/AsArrayConstraint.php b/src/Aggregate/AsArrayConstraint.php index b2ea42c..fc9e90c 100644 --- a/src/Aggregate/AsArrayConstraint.php +++ b/src/Aggregate/AsArrayConstraint.php @@ -57,7 +57,7 @@ public function __construct( /** * The element property name to which the constraint is applied * - * @var literal-string + * @var non-empty-string * @readonly */ private string $target, diff --git a/src/Aggregate/CallbackArrayConstraint.php b/src/Aggregate/CallbackArrayConstraint.php index 1be3d49..bb83539 100644 --- a/src/Aggregate/CallbackArrayConstraint.php +++ b/src/Aggregate/CallbackArrayConstraint.php @@ -59,7 +59,7 @@ public function __construct( * - Return string for a custom error message * - Return array with error message and code * - * @var literal-string + * @var non-empty-string * @readonly */ private string $methodName, From 0f92f387da85b1354941faebc51250670a65fdba Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 26 Jul 2024 11:37:33 +0200 Subject: [PATCH 29/40] doc: Update README with new attributes --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 77b47ff..d5e310b 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,16 @@ $processor->generate(new MyOtherForm()); | [`CallbackGenerator`](src/Form/CallbackGenerator.php) | `CallbackGenerator('generate')` | `$builder->generates([$this, 'generate'])` | Define the method to use for generate the form value. The method must be declared as public on the form class. | | [`Csrf`](src/Form/Csrf.php) | `Csrf(tokenId: 'MyToken')` | `$builder->csrf()->tokenId('MyToken')` | Add a CSRF element on the form. | +### On method + +| Attribute | Example | Translated to | Purpose | +|------------------------------------------------------------|--------------------------------------|-------------------------------------------------------|-------------------------------------------------------------| +| [`AsConstraint`](src/Constraint/AsConstraint.php) | `AsConstraint('validateFoo')` | `$builder->satisfy([$this, 'validateFoo'])` | Use the method as constraint for the target element. | +| [`AsArrayConstraint`](src/Aggregate/AsArrayConstraint.php) | `AsArrayConstraint('validateFoo')` | `$builder->arrayConstraint([$this, 'validateFoo'])` | Use the method as constraint for the target array element. | +| [`AsFilter`](src/Child/AsFilter.php) | `AsFilter('filterFoo')` | `$builder->filter([$this, 'filterFoo'])` | Use the method as filter for the target element. | +| [`AsTransformer`](src/Element/AsTransformer.php) | `AsTransformer('transformFoo')` | `$builder->transformer([$this, 'transformFoo'])` | Use the method as HTTP transformer for the target element. | +| [`AsModelTransformer`](src/Child/AsModelTransformer.php) | `AsModelTransformer('transformFoo')` | `$builder->modelTransformer([$this, 'transformFoo'])` | Use the method as model transformer for the target element. | + ### On button property | Attribute | Example | Translated to | Purpose | @@ -153,6 +163,8 @@ $processor->generate(new MyOtherForm()); | [`DefaultValue`](src/Child/DefaultValue.php) | `DefaultValue(42)` | `...->configureField($elementBuilder)` | Define the default value of the input. | | [`Dependencies`](src/Child/Dependencies.php) | `Dependencies('foo', 'bar')` | `...->depends('foo', 'bar')` | Declare dependencies on the current input. Dependencies will be submitted before the current field. | | [`GetSet`](src/Child/GetSet.php) | `GetSet('realField')` | `...->getter('realField')->setter('realField')` | Enable hydration and extraction of the entity. | +| [`CallbackFilter`](src/Child/CallbackFilter.php) | `CallbackFilter('filterMethod')` | `...->filter([$this, 'filterMethod'])` | Add a filter on the current child using a method. | +| [`HttpField`](src/Child/HttpField.php) | `HttpField('_field')` | `...->httpField(new ArrayOffsetHttpField('_field'))` | Define the http field name to use on the current child, instead of use the property name. | | **Element** | | | | | [`CallbackConstraint`](src/Constraint/CallbackConstraint.php) | `CallbackConstraint('validateInput')` | `...->satisfy([$this, 'validateInput'])` | Validate an input using a method. | | [`Satisfy`](src/Constraint/Satisfy.php) | `Satisfy(MyConstraint::class, ['opt' => 'val'])` | `...->satisfy(new MyConstraint(['opt' => 'val']))` | Add a constraint on the input. Prefer directly use the constraint class as attribute if possible. | @@ -162,13 +174,17 @@ $processor->generate(new MyOtherForm()); | [`Raw`](src/Element/Raw.php) | `Raw` | `...->raw()` | For number elements. Use native PHP cast instead of locale parsing for convert number. | | [`TransformerError`](src/Element/TransformerError.php) | `TransformerError(message: 'Invalid value provided')` | `...->transformerErrorMessage('Invalid value provided')` | Configure error handling of transformer exceptions. | | [`IgnoreTransformerException`](src/Element/IgnoreTransformerException.php) | `IgnoreTransformerException` | `...->ignoreTransformerException()` | Ignore transformer exception. If enable and an exception occurs, the raw value will be used. | +| [`Required`](src/Element/Required.php) | `Required` | `...->required()` | Mark the element as required. The error message can be defined as parameter of the attribute. | | **DateTimeElement** | | | | | [`DateFormat`](src/Element/Date/DateFormat.php) | `DateFormat('d/m/Y H:i')` | `...->format('d/m/Y H:i')` | Define the input date format. | | [`DateTimeClass`](src/Element/Date/DateTimeClass.php) | `DateTimeClass(Carbon::class)` | `...->className(Carbon::class)` | Define date time class to use on for parse the date. | | [`ImmutableDateTime`](src/Element/Date/ImmutableDateTime.php) | `ImmutableDateTime` | `...->immutable()` | Use `DateTimeImmutable` as date time class. | | [`Timezone`](src/Element/Date/Timezone.php) | `Timezone('Europe/Paris')` | `...->timezone('Europe/Paris')` | Define the parsing and normalized timezone to use. | +| [`AfterField`](src/Element/Date/AfterField.php) | `AfterField('otherField')` | `...->afterField('otherField')` | Add a greater than an other field constraint to the current element. | +| [`BeforeField`](src/Element/Date/BeforeField.php) | `BeforeField('otherField')` | `...->beforeField('otherField')` | Add a less than an other field constraint to the current element. | | **ArrayElement** | | | | | [`ArrayConstraint`](src/Aggregate/ArrayConstraint.php) | `ArrayConstraint(MyConstraint::class, ['opt' => 'val'])` | `...->arrayConstraint(new MyConstraint(['opt' => 'val']))` | Add a constraint on the whole array element. | | [`CallbackArrayConstraint`](src/Aggregate/CallbackArrayConstraint.php) | `CallbackArrayConstraint('validateInput')` | `...->arrayConstraint([$this, 'validateInput'])` | Add a constraint on the whole array element, using a form method. | | [`Count`](src/Aggregate/Count.php) | `Count(min: 3, max: 6)` | `...->arrayConstraint(new Count(min: 3, max: 6))` | Add a Count constraint on the array element. | | [`ElementType`](src/Aggregate/ElementType.php) | `ElementType(IntegerElement::class, 'configureElement')` | `...->element(IntegerElement::class, [$this, 'configureElement'])` | Define the array element type. A configuration callback method can be define for configure the inner element. | +| [`ArrayTransformer`](src/Aggregate/ArrayTransformer.php) | `ArrayTransformer(MyTransformer::class, ['ctroarg'])` | `...->arrayTransformer(new MyTransformer('ctorarg))` | Add a transformer for the whole array input. | From 7426b937519fc7f6434c42c88110b1ee26c654bc Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 11 Oct 2024 10:09:42 +0200 Subject: [PATCH 30/40] chore: Compatibility with PHP 8.4 (#FRAM-177) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d8d896f..766b305 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ }, "minimum-stability": "dev", "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "b2pweb/bdf-form": "~1.1", "nette/php-generator": "~3.6|~4.0" }, From 8f8f3fab8e196bbcf4854c0fc456db496e7283fa Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Tue, 3 Dec 2024 14:47:30 +0100 Subject: [PATCH 31/40] ci: test with php 8.4 --- .github/workflows/php.yml | 2 +- composer.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 252913b..13f24b2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.0', '8.1', '8.2', '8.3'] + php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: diff --git a/composer.json b/composer.json index 766b305..b443805 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ } }, "minimum-stability": "dev", + "prefer-stable": true, "require": { "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "b2pweb/bdf-form": "~1.1", @@ -28,7 +29,7 @@ }, "require-dev": { "phpunit/phpunit": "~9.5", - "vimeo/psalm": "~5.22", + "vimeo/psalm": "~5.22 | ~6.0", "squizlabs/php_codesniffer": "~3.6.1", "symfony/security-csrf": "~5.4|~6.1" }, From b29d6f037f49bfededc20120ba2ee1daf94cf423 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Tue, 3 Dec 2024 15:06:26 +0100 Subject: [PATCH 32/40] ci: try to fix infection tests --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 13f24b2..e5f7917 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -75,4 +75,4 @@ jobs: - name: Run Infection run: | git fetch --depth=1 origin $GITHUB_BASE_REF - ./infection.phar --logger-github --git-diff-filter=AM + XDEBUG_MODE=coverage ./infection.phar --logger-github --git-diff-filter=AM From 79ea2528b6c15602d040685574950f9d148f3abf Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Tue, 3 Dec 2024 15:39:21 +0100 Subject: [PATCH 33/40] ci: update infection --- .github/workflows/php.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index e5f7917..f47ffcf 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -54,8 +54,8 @@ jobs: - name: Install Infection run: | - wget https://github.com/infection/infection/releases/download/0.21.5/infection.phar - wget https://github.com/infection/infection/releases/download/0.21.5/infection.phar.asc + wget https://github.com/infection/infection/releases/download/0.25.6/infection.phar + wget https://github.com/infection/infection/releases/download/0.25.6/infection.phar.asc chmod +x infection.phar gpg --recv-keys C6D76C329EBADE2FB9C458CFC5095986493B4AA0 gpg --with-fingerprint --verify infection.phar.asc infection.phar From b8e137e6b4452904f02a1c83a49684c08315e731 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Fri, 21 Nov 2025 10:39:57 +0100 Subject: [PATCH 34/40] chore: compatibility with PHP 8.5 --- composer.json | 2 +- src/Processor/ReflectionProcessor.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b443805..926caf3 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "b2pweb/bdf-form": "~1.1", "nette/php-generator": "~3.6|~4.0" }, diff --git a/src/Processor/ReflectionProcessor.php b/src/Processor/ReflectionProcessor.php index e620d80..ccdafcc 100644 --- a/src/Processor/ReflectionProcessor.php +++ b/src/Processor/ReflectionProcessor.php @@ -58,7 +58,7 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil } $elementType = $property->getType()->getName(); - $property->setAccessible(true); + PHP_VERSION_ID >= 80100 or $property->setAccessible(true); if ($elementType === ButtonInterface::class) { $buttonProperties[$name] = $property; From ad3f86db4e386046ecb099056214d03f399e80ab Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 18 Mar 2026 12:21:13 +0100 Subject: [PATCH 35/40] chore: Prepare merge with bdf-form --- composer.json | 4 ++-- phpunit.xml.dist | 4 ---- src/{ => Attribute}/Aggregate/ArrayConstraint.php | 0 src/{ => Attribute}/Aggregate/ArrayTransformer.php | 0 src/{ => Attribute}/Aggregate/AsArrayConstraint.php | 0 src/{ => Attribute}/Aggregate/CallbackArrayConstraint.php | 0 src/{ => Attribute}/Aggregate/Count.php | 0 src/{ => Attribute}/Aggregate/ElementType.php | 0 src/{ => Attribute}/AttributeForm.php | 0 .../Button/ButtonBuilderAttributeInterface.php | 0 src/{ => Attribute}/Button/Groups.php | 0 src/{ => Attribute}/Button/Value.php | 0 src/{ => Attribute}/Child/AsFilter.php | 0 src/{ => Attribute}/Child/AsModelTransformer.php | 0 src/{ => Attribute}/Child/CallbackFilter.php | 0 src/{ => Attribute}/Child/CallbackModelTransformer.php | 0 src/{ => Attribute}/Child/Configure.php | 0 src/{ => Attribute}/Child/DefaultValue.php | 0 src/{ => Attribute}/Child/Dependencies.php | 0 src/{ => Attribute}/Child/GetSet.php | 0 src/{ => Attribute}/Child/HttpField.php | 0 src/{ => Attribute}/Child/ModelTransformer.php | 0 src/{ => Attribute}/ChildBuilderAttributeInterface.php | 0 src/{ => Attribute}/Constraint/AsConstraint.php | 0 src/{ => Attribute}/Constraint/CallbackConstraint.php | 0 src/{ => Attribute}/Constraint/Satisfy.php | 0 src/{ => Attribute}/Element/AsTransformer.php | 0 src/{ => Attribute}/Element/CallbackTransformer.php | 0 src/{ => Attribute}/Element/Choices.php | 0 src/{ => Attribute}/Element/Date/AfterField.php | 0 src/{ => Attribute}/Element/Date/BeforeField.php | 0 src/{ => Attribute}/Element/Date/DateFormat.php | 0 src/{ => Attribute}/Element/Date/DateTimeClass.php | 0 src/{ => Attribute}/Element/Date/ImmutableDateTime.php | 0 src/{ => Attribute}/Element/Date/Timezone.php | 0 src/{ => Attribute}/Element/IgnoreTransformerException.php | 0 src/{ => Attribute}/Element/Raw.php | 0 src/{ => Attribute}/Element/Required.php | 0 src/{ => Attribute}/Element/Transformer.php | 0 src/{ => Attribute}/Element/TransformerError.php | 0 src/{ => Attribute}/Form/CallbackGenerator.php | 0 src/{ => Attribute}/Form/Csrf.php | 0 src/{ => Attribute}/Form/FormBuilderAttributeInterface.php | 0 src/{ => Attribute}/Form/Generates.php | 0 .../Processor/AttributesProcessorInterface.php | 0 .../Processor/CodeGenerator/AttributesProcessorGenerator.php | 0 .../Processor/CodeGenerator/ClassGenerator.php | 0 .../Processor/CodeGenerator/ObjectInstantiation.php | 0 .../Processor/CodeGenerator/TransformerClassGenerator.php | 0 src/{ => Attribute}/Processor/CompileAttributesProcessor.php | 0 .../Processor/ConfigureFormBuilderStrategy.php | 0 .../Processor/Element/ConstraintAttributeProcessor.php | 0 .../Processor/Element/ElementAttributeProcessorInterface.php | 0 .../Processor/Element/ExtractorAttributeProcessor.php | 0 .../Processor/Element/FilterAttributeProcessor.php | 0 .../Processor/Element/HydratorAttributeProcessor.php | 0 .../Processor/Element/SimpleMethodCallGeneratorTrait.php | 0 .../Processor/Element/TransformerAttributeProcessor.php | 0 .../Processor/GenerateConfiguratorStrategy.php | 0 .../Processor/MethodChildBuilderAttributeInterface.php | 0 src/{ => Attribute}/Processor/PostConfigureInterface.php | 0 .../Processor/PostConfigureReflectionSetProperties.php | 0 src/{ => Attribute}/Processor/ProcessorMetadata.php | 0 src/{ => Attribute}/Processor/ReflectionProcessor.php | 0 src/{ => Attribute}/Processor/ReflectionStrategyInterface.php | 0 tests/{ => Attribute}/Aggregate/ArrayConstraintTest.php | 0 tests/{ => Attribute}/Aggregate/ArrayTransformerTest.php | 0 tests/{ => Attribute}/Aggregate/AsArrayConstraintTest.php | 0 .../{ => Attribute}/Aggregate/CallbackArrayConstraintTest.php | 0 tests/{ => Attribute}/Aggregate/CountTest.php | 0 tests/{ => Attribute}/Aggregate/ElementTypeTest.php | 0 tests/{ => Attribute}/Button/GroupsTest.php | 0 tests/{ => Attribute}/Button/ValueTest.php | 0 tests/{ => Attribute}/Child/AsFilterTest.php | 0 tests/{ => Attribute}/Child/AsModelTransformerTest.php | 0 tests/{ => Attribute}/Child/CallbackFilterTest.php | 0 tests/{ => Attribute}/Child/CallbackModelTransformerTest.php | 0 tests/{ => Attribute}/Child/ConfigureTest.php | 0 tests/{ => Attribute}/Child/DefaultValueTest.php | 0 tests/{ => Attribute}/Child/DependenciesTest.php | 0 tests/{ => Attribute}/Child/GetSetTest.php | 0 tests/{ => Attribute}/Child/HttpFieldTest.php | 0 tests/{ => Attribute}/Child/ModelTransformerTest.php | 0 tests/{ => Attribute}/Constraint/AsConstraintTest.php | 0 tests/{ => Attribute}/Constraint/CallbackConstraintTest.php | 0 tests/{ => Attribute}/Constraint/SatisfyTest.php | 0 tests/{ => Attribute}/Element/AsTransformerTest.php | 0 tests/{ => Attribute}/Element/CallbackTransformerTest.php | 0 tests/{ => Attribute}/Element/ChoicesTest.php | 0 tests/{ => Attribute}/Element/Date/AfterFieldTest.php | 0 tests/{ => Attribute}/Element/Date/BeforeFieldTest.php | 0 tests/{ => Attribute}/Element/Date/DateFormatTest.php | 0 tests/{ => Attribute}/Element/Date/DateTimeClassTest.php | 0 tests/{ => Attribute}/Element/Date/ImmtableDateTimeTest.php | 0 tests/{ => Attribute}/Element/Date/TimezoneTest.php | 0 .../Element/IgnoreTransformerExceptionTest.php | 0 tests/{ => Attribute}/Element/RawTest.php | 0 tests/{ => Attribute}/Element/RequiredTest.php | 0 tests/{ => Attribute}/Element/TransformerErrorTest.php | 0 tests/{ => Attribute}/Element/TransformerTest.php | 0 tests/{ => Attribute}/Form/CallbackGeneratorTest.php | 0 tests/{ => Attribute}/Form/CsrfTest.php | 0 tests/{ => Attribute}/Form/GeneratesTest.php | 0 tests/{ => Attribute}/FunctionalTest.php | 0 tests/{ => Attribute}/Php81/Aggregate/ArrayConstraintTest.php | 0 .../Php81/Aggregate/CallbackArrayConstraintTest.php | 0 tests/{ => Attribute}/Php81/Constraint/SatisfyTest.php | 0 .../CodeGenerator/AttributesProcessorGeneratorTest.php | 0 .../Processor/CodeGenerator/ClassGeneratorTest.php | 0 .../Processor/CodeGenerator/ObjectInstantiationTest.php | 0 .../Processor/CodeGenerator/TransformerClassGeneratorTest.php | 0 .../Processor/CompileAttributesProcessorTest.php | 0 .../Processor/Element/ConstraintAttributeProcessorTest.php | 0 .../Processor/Element/ExtractorAttributeProcessorTest.php | 0 .../Processor/Element/FilterAttributeProcessorTest.php | 0 .../Processor/Element/HydratorAttributeProcessorTest.php | 0 .../Processor/Element/TransformerAttributeProcessorTest.php | 0 .../Processor/GenerateConfiguratorStrategyTest.php | 0 tests/{ => Attribute}/Processor/ReflectionProcessorTest.php | 0 tests/{ => Attribute}/TestCase.php | 0 120 files changed, 2 insertions(+), 6 deletions(-) rename src/{ => Attribute}/Aggregate/ArrayConstraint.php (100%) rename src/{ => Attribute}/Aggregate/ArrayTransformer.php (100%) rename src/{ => Attribute}/Aggregate/AsArrayConstraint.php (100%) rename src/{ => Attribute}/Aggregate/CallbackArrayConstraint.php (100%) rename src/{ => Attribute}/Aggregate/Count.php (100%) rename src/{ => Attribute}/Aggregate/ElementType.php (100%) rename src/{ => Attribute}/AttributeForm.php (100%) rename src/{ => Attribute}/Button/ButtonBuilderAttributeInterface.php (100%) rename src/{ => Attribute}/Button/Groups.php (100%) rename src/{ => Attribute}/Button/Value.php (100%) rename src/{ => Attribute}/Child/AsFilter.php (100%) rename src/{ => Attribute}/Child/AsModelTransformer.php (100%) rename src/{ => Attribute}/Child/CallbackFilter.php (100%) rename src/{ => Attribute}/Child/CallbackModelTransformer.php (100%) rename src/{ => Attribute}/Child/Configure.php (100%) rename src/{ => Attribute}/Child/DefaultValue.php (100%) rename src/{ => Attribute}/Child/Dependencies.php (100%) rename src/{ => Attribute}/Child/GetSet.php (100%) rename src/{ => Attribute}/Child/HttpField.php (100%) rename src/{ => Attribute}/Child/ModelTransformer.php (100%) rename src/{ => Attribute}/ChildBuilderAttributeInterface.php (100%) rename src/{ => Attribute}/Constraint/AsConstraint.php (100%) rename src/{ => Attribute}/Constraint/CallbackConstraint.php (100%) rename src/{ => Attribute}/Constraint/Satisfy.php (100%) rename src/{ => Attribute}/Element/AsTransformer.php (100%) rename src/{ => Attribute}/Element/CallbackTransformer.php (100%) rename src/{ => Attribute}/Element/Choices.php (100%) rename src/{ => Attribute}/Element/Date/AfterField.php (100%) rename src/{ => Attribute}/Element/Date/BeforeField.php (100%) rename src/{ => Attribute}/Element/Date/DateFormat.php (100%) rename src/{ => Attribute}/Element/Date/DateTimeClass.php (100%) rename src/{ => Attribute}/Element/Date/ImmutableDateTime.php (100%) rename src/{ => Attribute}/Element/Date/Timezone.php (100%) rename src/{ => Attribute}/Element/IgnoreTransformerException.php (100%) rename src/{ => Attribute}/Element/Raw.php (100%) rename src/{ => Attribute}/Element/Required.php (100%) rename src/{ => Attribute}/Element/Transformer.php (100%) rename src/{ => Attribute}/Element/TransformerError.php (100%) rename src/{ => Attribute}/Form/CallbackGenerator.php (100%) rename src/{ => Attribute}/Form/Csrf.php (100%) rename src/{ => Attribute}/Form/FormBuilderAttributeInterface.php (100%) rename src/{ => Attribute}/Form/Generates.php (100%) rename src/{ => Attribute}/Processor/AttributesProcessorInterface.php (100%) rename src/{ => Attribute}/Processor/CodeGenerator/AttributesProcessorGenerator.php (100%) rename src/{ => Attribute}/Processor/CodeGenerator/ClassGenerator.php (100%) rename src/{ => Attribute}/Processor/CodeGenerator/ObjectInstantiation.php (100%) rename src/{ => Attribute}/Processor/CodeGenerator/TransformerClassGenerator.php (100%) rename src/{ => Attribute}/Processor/CompileAttributesProcessor.php (100%) rename src/{ => Attribute}/Processor/ConfigureFormBuilderStrategy.php (100%) rename src/{ => Attribute}/Processor/Element/ConstraintAttributeProcessor.php (100%) rename src/{ => Attribute}/Processor/Element/ElementAttributeProcessorInterface.php (100%) rename src/{ => Attribute}/Processor/Element/ExtractorAttributeProcessor.php (100%) rename src/{ => Attribute}/Processor/Element/FilterAttributeProcessor.php (100%) rename src/{ => Attribute}/Processor/Element/HydratorAttributeProcessor.php (100%) rename src/{ => Attribute}/Processor/Element/SimpleMethodCallGeneratorTrait.php (100%) rename src/{ => Attribute}/Processor/Element/TransformerAttributeProcessor.php (100%) rename src/{ => Attribute}/Processor/GenerateConfiguratorStrategy.php (100%) rename src/{ => Attribute}/Processor/MethodChildBuilderAttributeInterface.php (100%) rename src/{ => Attribute}/Processor/PostConfigureInterface.php (100%) rename src/{ => Attribute}/Processor/PostConfigureReflectionSetProperties.php (100%) rename src/{ => Attribute}/Processor/ProcessorMetadata.php (100%) rename src/{ => Attribute}/Processor/ReflectionProcessor.php (100%) rename src/{ => Attribute}/Processor/ReflectionStrategyInterface.php (100%) rename tests/{ => Attribute}/Aggregate/ArrayConstraintTest.php (100%) rename tests/{ => Attribute}/Aggregate/ArrayTransformerTest.php (100%) rename tests/{ => Attribute}/Aggregate/AsArrayConstraintTest.php (100%) rename tests/{ => Attribute}/Aggregate/CallbackArrayConstraintTest.php (100%) rename tests/{ => Attribute}/Aggregate/CountTest.php (100%) rename tests/{ => Attribute}/Aggregate/ElementTypeTest.php (100%) rename tests/{ => Attribute}/Button/GroupsTest.php (100%) rename tests/{ => Attribute}/Button/ValueTest.php (100%) rename tests/{ => Attribute}/Child/AsFilterTest.php (100%) rename tests/{ => Attribute}/Child/AsModelTransformerTest.php (100%) rename tests/{ => Attribute}/Child/CallbackFilterTest.php (100%) rename tests/{ => Attribute}/Child/CallbackModelTransformerTest.php (100%) rename tests/{ => Attribute}/Child/ConfigureTest.php (100%) rename tests/{ => Attribute}/Child/DefaultValueTest.php (100%) rename tests/{ => Attribute}/Child/DependenciesTest.php (100%) rename tests/{ => Attribute}/Child/GetSetTest.php (100%) rename tests/{ => Attribute}/Child/HttpFieldTest.php (100%) rename tests/{ => Attribute}/Child/ModelTransformerTest.php (100%) rename tests/{ => Attribute}/Constraint/AsConstraintTest.php (100%) rename tests/{ => Attribute}/Constraint/CallbackConstraintTest.php (100%) rename tests/{ => Attribute}/Constraint/SatisfyTest.php (100%) rename tests/{ => Attribute}/Element/AsTransformerTest.php (100%) rename tests/{ => Attribute}/Element/CallbackTransformerTest.php (100%) rename tests/{ => Attribute}/Element/ChoicesTest.php (100%) rename tests/{ => Attribute}/Element/Date/AfterFieldTest.php (100%) rename tests/{ => Attribute}/Element/Date/BeforeFieldTest.php (100%) rename tests/{ => Attribute}/Element/Date/DateFormatTest.php (100%) rename tests/{ => Attribute}/Element/Date/DateTimeClassTest.php (100%) rename tests/{ => Attribute}/Element/Date/ImmtableDateTimeTest.php (100%) rename tests/{ => Attribute}/Element/Date/TimezoneTest.php (100%) rename tests/{ => Attribute}/Element/IgnoreTransformerExceptionTest.php (100%) rename tests/{ => Attribute}/Element/RawTest.php (100%) rename tests/{ => Attribute}/Element/RequiredTest.php (100%) rename tests/{ => Attribute}/Element/TransformerErrorTest.php (100%) rename tests/{ => Attribute}/Element/TransformerTest.php (100%) rename tests/{ => Attribute}/Form/CallbackGeneratorTest.php (100%) rename tests/{ => Attribute}/Form/CsrfTest.php (100%) rename tests/{ => Attribute}/Form/GeneratesTest.php (100%) rename tests/{ => Attribute}/FunctionalTest.php (100%) rename tests/{ => Attribute}/Php81/Aggregate/ArrayConstraintTest.php (100%) rename tests/{ => Attribute}/Php81/Aggregate/CallbackArrayConstraintTest.php (100%) rename tests/{ => Attribute}/Php81/Constraint/SatisfyTest.php (100%) rename tests/{ => Attribute}/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php (100%) rename tests/{ => Attribute}/Processor/CodeGenerator/ClassGeneratorTest.php (100%) rename tests/{ => Attribute}/Processor/CodeGenerator/ObjectInstantiationTest.php (100%) rename tests/{ => Attribute}/Processor/CodeGenerator/TransformerClassGeneratorTest.php (100%) rename tests/{ => Attribute}/Processor/CompileAttributesProcessorTest.php (100%) rename tests/{ => Attribute}/Processor/Element/ConstraintAttributeProcessorTest.php (100%) rename tests/{ => Attribute}/Processor/Element/ExtractorAttributeProcessorTest.php (100%) rename tests/{ => Attribute}/Processor/Element/FilterAttributeProcessorTest.php (100%) rename tests/{ => Attribute}/Processor/Element/HydratorAttributeProcessorTest.php (100%) rename tests/{ => Attribute}/Processor/Element/TransformerAttributeProcessorTest.php (100%) rename tests/{ => Attribute}/Processor/GenerateConfiguratorStrategyTest.php (100%) rename tests/{ => Attribute}/Processor/ReflectionProcessorTest.php (100%) rename tests/{ => Attribute}/TestCase.php (100%) diff --git a/composer.json b/composer.json index 926caf3..374c59f 100644 --- a/composer.json +++ b/composer.json @@ -12,12 +12,12 @@ "type": "library", "autoload": { "psr-4": { - "Bdf\\Form\\Attribute\\": "src" + "Bdf\\Form\\": "src" } }, "autoload-dev": { "psr-4": { - "Tests\\Form\\Attribute\\": "tests" + "Tests\\Form\\": "tests" } }, "minimum-stability": "dev", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3b8f2ad..a9e870d 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,10 +21,6 @@ ./tests - ./tests/Php81 - - - ./tests/Php81 diff --git a/src/Aggregate/ArrayConstraint.php b/src/Attribute/Aggregate/ArrayConstraint.php similarity index 100% rename from src/Aggregate/ArrayConstraint.php rename to src/Attribute/Aggregate/ArrayConstraint.php diff --git a/src/Aggregate/ArrayTransformer.php b/src/Attribute/Aggregate/ArrayTransformer.php similarity index 100% rename from src/Aggregate/ArrayTransformer.php rename to src/Attribute/Aggregate/ArrayTransformer.php diff --git a/src/Aggregate/AsArrayConstraint.php b/src/Attribute/Aggregate/AsArrayConstraint.php similarity index 100% rename from src/Aggregate/AsArrayConstraint.php rename to src/Attribute/Aggregate/AsArrayConstraint.php diff --git a/src/Aggregate/CallbackArrayConstraint.php b/src/Attribute/Aggregate/CallbackArrayConstraint.php similarity index 100% rename from src/Aggregate/CallbackArrayConstraint.php rename to src/Attribute/Aggregate/CallbackArrayConstraint.php diff --git a/src/Aggregate/Count.php b/src/Attribute/Aggregate/Count.php similarity index 100% rename from src/Aggregate/Count.php rename to src/Attribute/Aggregate/Count.php diff --git a/src/Aggregate/ElementType.php b/src/Attribute/Aggregate/ElementType.php similarity index 100% rename from src/Aggregate/ElementType.php rename to src/Attribute/Aggregate/ElementType.php diff --git a/src/AttributeForm.php b/src/Attribute/AttributeForm.php similarity index 100% rename from src/AttributeForm.php rename to src/Attribute/AttributeForm.php diff --git a/src/Button/ButtonBuilderAttributeInterface.php b/src/Attribute/Button/ButtonBuilderAttributeInterface.php similarity index 100% rename from src/Button/ButtonBuilderAttributeInterface.php rename to src/Attribute/Button/ButtonBuilderAttributeInterface.php diff --git a/src/Button/Groups.php b/src/Attribute/Button/Groups.php similarity index 100% rename from src/Button/Groups.php rename to src/Attribute/Button/Groups.php diff --git a/src/Button/Value.php b/src/Attribute/Button/Value.php similarity index 100% rename from src/Button/Value.php rename to src/Attribute/Button/Value.php diff --git a/src/Child/AsFilter.php b/src/Attribute/Child/AsFilter.php similarity index 100% rename from src/Child/AsFilter.php rename to src/Attribute/Child/AsFilter.php diff --git a/src/Child/AsModelTransformer.php b/src/Attribute/Child/AsModelTransformer.php similarity index 100% rename from src/Child/AsModelTransformer.php rename to src/Attribute/Child/AsModelTransformer.php diff --git a/src/Child/CallbackFilter.php b/src/Attribute/Child/CallbackFilter.php similarity index 100% rename from src/Child/CallbackFilter.php rename to src/Attribute/Child/CallbackFilter.php diff --git a/src/Child/CallbackModelTransformer.php b/src/Attribute/Child/CallbackModelTransformer.php similarity index 100% rename from src/Child/CallbackModelTransformer.php rename to src/Attribute/Child/CallbackModelTransformer.php diff --git a/src/Child/Configure.php b/src/Attribute/Child/Configure.php similarity index 100% rename from src/Child/Configure.php rename to src/Attribute/Child/Configure.php diff --git a/src/Child/DefaultValue.php b/src/Attribute/Child/DefaultValue.php similarity index 100% rename from src/Child/DefaultValue.php rename to src/Attribute/Child/DefaultValue.php diff --git a/src/Child/Dependencies.php b/src/Attribute/Child/Dependencies.php similarity index 100% rename from src/Child/Dependencies.php rename to src/Attribute/Child/Dependencies.php diff --git a/src/Child/GetSet.php b/src/Attribute/Child/GetSet.php similarity index 100% rename from src/Child/GetSet.php rename to src/Attribute/Child/GetSet.php diff --git a/src/Child/HttpField.php b/src/Attribute/Child/HttpField.php similarity index 100% rename from src/Child/HttpField.php rename to src/Attribute/Child/HttpField.php diff --git a/src/Child/ModelTransformer.php b/src/Attribute/Child/ModelTransformer.php similarity index 100% rename from src/Child/ModelTransformer.php rename to src/Attribute/Child/ModelTransformer.php diff --git a/src/ChildBuilderAttributeInterface.php b/src/Attribute/ChildBuilderAttributeInterface.php similarity index 100% rename from src/ChildBuilderAttributeInterface.php rename to src/Attribute/ChildBuilderAttributeInterface.php diff --git a/src/Constraint/AsConstraint.php b/src/Attribute/Constraint/AsConstraint.php similarity index 100% rename from src/Constraint/AsConstraint.php rename to src/Attribute/Constraint/AsConstraint.php diff --git a/src/Constraint/CallbackConstraint.php b/src/Attribute/Constraint/CallbackConstraint.php similarity index 100% rename from src/Constraint/CallbackConstraint.php rename to src/Attribute/Constraint/CallbackConstraint.php diff --git a/src/Constraint/Satisfy.php b/src/Attribute/Constraint/Satisfy.php similarity index 100% rename from src/Constraint/Satisfy.php rename to src/Attribute/Constraint/Satisfy.php diff --git a/src/Element/AsTransformer.php b/src/Attribute/Element/AsTransformer.php similarity index 100% rename from src/Element/AsTransformer.php rename to src/Attribute/Element/AsTransformer.php diff --git a/src/Element/CallbackTransformer.php b/src/Attribute/Element/CallbackTransformer.php similarity index 100% rename from src/Element/CallbackTransformer.php rename to src/Attribute/Element/CallbackTransformer.php diff --git a/src/Element/Choices.php b/src/Attribute/Element/Choices.php similarity index 100% rename from src/Element/Choices.php rename to src/Attribute/Element/Choices.php diff --git a/src/Element/Date/AfterField.php b/src/Attribute/Element/Date/AfterField.php similarity index 100% rename from src/Element/Date/AfterField.php rename to src/Attribute/Element/Date/AfterField.php diff --git a/src/Element/Date/BeforeField.php b/src/Attribute/Element/Date/BeforeField.php similarity index 100% rename from src/Element/Date/BeforeField.php rename to src/Attribute/Element/Date/BeforeField.php diff --git a/src/Element/Date/DateFormat.php b/src/Attribute/Element/Date/DateFormat.php similarity index 100% rename from src/Element/Date/DateFormat.php rename to src/Attribute/Element/Date/DateFormat.php diff --git a/src/Element/Date/DateTimeClass.php b/src/Attribute/Element/Date/DateTimeClass.php similarity index 100% rename from src/Element/Date/DateTimeClass.php rename to src/Attribute/Element/Date/DateTimeClass.php diff --git a/src/Element/Date/ImmutableDateTime.php b/src/Attribute/Element/Date/ImmutableDateTime.php similarity index 100% rename from src/Element/Date/ImmutableDateTime.php rename to src/Attribute/Element/Date/ImmutableDateTime.php diff --git a/src/Element/Date/Timezone.php b/src/Attribute/Element/Date/Timezone.php similarity index 100% rename from src/Element/Date/Timezone.php rename to src/Attribute/Element/Date/Timezone.php diff --git a/src/Element/IgnoreTransformerException.php b/src/Attribute/Element/IgnoreTransformerException.php similarity index 100% rename from src/Element/IgnoreTransformerException.php rename to src/Attribute/Element/IgnoreTransformerException.php diff --git a/src/Element/Raw.php b/src/Attribute/Element/Raw.php similarity index 100% rename from src/Element/Raw.php rename to src/Attribute/Element/Raw.php diff --git a/src/Element/Required.php b/src/Attribute/Element/Required.php similarity index 100% rename from src/Element/Required.php rename to src/Attribute/Element/Required.php diff --git a/src/Element/Transformer.php b/src/Attribute/Element/Transformer.php similarity index 100% rename from src/Element/Transformer.php rename to src/Attribute/Element/Transformer.php diff --git a/src/Element/TransformerError.php b/src/Attribute/Element/TransformerError.php similarity index 100% rename from src/Element/TransformerError.php rename to src/Attribute/Element/TransformerError.php diff --git a/src/Form/CallbackGenerator.php b/src/Attribute/Form/CallbackGenerator.php similarity index 100% rename from src/Form/CallbackGenerator.php rename to src/Attribute/Form/CallbackGenerator.php diff --git a/src/Form/Csrf.php b/src/Attribute/Form/Csrf.php similarity index 100% rename from src/Form/Csrf.php rename to src/Attribute/Form/Csrf.php diff --git a/src/Form/FormBuilderAttributeInterface.php b/src/Attribute/Form/FormBuilderAttributeInterface.php similarity index 100% rename from src/Form/FormBuilderAttributeInterface.php rename to src/Attribute/Form/FormBuilderAttributeInterface.php diff --git a/src/Form/Generates.php b/src/Attribute/Form/Generates.php similarity index 100% rename from src/Form/Generates.php rename to src/Attribute/Form/Generates.php diff --git a/src/Processor/AttributesProcessorInterface.php b/src/Attribute/Processor/AttributesProcessorInterface.php similarity index 100% rename from src/Processor/AttributesProcessorInterface.php rename to src/Attribute/Processor/AttributesProcessorInterface.php diff --git a/src/Processor/CodeGenerator/AttributesProcessorGenerator.php b/src/Attribute/Processor/CodeGenerator/AttributesProcessorGenerator.php similarity index 100% rename from src/Processor/CodeGenerator/AttributesProcessorGenerator.php rename to src/Attribute/Processor/CodeGenerator/AttributesProcessorGenerator.php diff --git a/src/Processor/CodeGenerator/ClassGenerator.php b/src/Attribute/Processor/CodeGenerator/ClassGenerator.php similarity index 100% rename from src/Processor/CodeGenerator/ClassGenerator.php rename to src/Attribute/Processor/CodeGenerator/ClassGenerator.php diff --git a/src/Processor/CodeGenerator/ObjectInstantiation.php b/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php similarity index 100% rename from src/Processor/CodeGenerator/ObjectInstantiation.php rename to src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php diff --git a/src/Processor/CodeGenerator/TransformerClassGenerator.php b/src/Attribute/Processor/CodeGenerator/TransformerClassGenerator.php similarity index 100% rename from src/Processor/CodeGenerator/TransformerClassGenerator.php rename to src/Attribute/Processor/CodeGenerator/TransformerClassGenerator.php diff --git a/src/Processor/CompileAttributesProcessor.php b/src/Attribute/Processor/CompileAttributesProcessor.php similarity index 100% rename from src/Processor/CompileAttributesProcessor.php rename to src/Attribute/Processor/CompileAttributesProcessor.php diff --git a/src/Processor/ConfigureFormBuilderStrategy.php b/src/Attribute/Processor/ConfigureFormBuilderStrategy.php similarity index 100% rename from src/Processor/ConfigureFormBuilderStrategy.php rename to src/Attribute/Processor/ConfigureFormBuilderStrategy.php diff --git a/src/Processor/Element/ConstraintAttributeProcessor.php b/src/Attribute/Processor/Element/ConstraintAttributeProcessor.php similarity index 100% rename from src/Processor/Element/ConstraintAttributeProcessor.php rename to src/Attribute/Processor/Element/ConstraintAttributeProcessor.php diff --git a/src/Processor/Element/ElementAttributeProcessorInterface.php b/src/Attribute/Processor/Element/ElementAttributeProcessorInterface.php similarity index 100% rename from src/Processor/Element/ElementAttributeProcessorInterface.php rename to src/Attribute/Processor/Element/ElementAttributeProcessorInterface.php diff --git a/src/Processor/Element/ExtractorAttributeProcessor.php b/src/Attribute/Processor/Element/ExtractorAttributeProcessor.php similarity index 100% rename from src/Processor/Element/ExtractorAttributeProcessor.php rename to src/Attribute/Processor/Element/ExtractorAttributeProcessor.php diff --git a/src/Processor/Element/FilterAttributeProcessor.php b/src/Attribute/Processor/Element/FilterAttributeProcessor.php similarity index 100% rename from src/Processor/Element/FilterAttributeProcessor.php rename to src/Attribute/Processor/Element/FilterAttributeProcessor.php diff --git a/src/Processor/Element/HydratorAttributeProcessor.php b/src/Attribute/Processor/Element/HydratorAttributeProcessor.php similarity index 100% rename from src/Processor/Element/HydratorAttributeProcessor.php rename to src/Attribute/Processor/Element/HydratorAttributeProcessor.php diff --git a/src/Processor/Element/SimpleMethodCallGeneratorTrait.php b/src/Attribute/Processor/Element/SimpleMethodCallGeneratorTrait.php similarity index 100% rename from src/Processor/Element/SimpleMethodCallGeneratorTrait.php rename to src/Attribute/Processor/Element/SimpleMethodCallGeneratorTrait.php diff --git a/src/Processor/Element/TransformerAttributeProcessor.php b/src/Attribute/Processor/Element/TransformerAttributeProcessor.php similarity index 100% rename from src/Processor/Element/TransformerAttributeProcessor.php rename to src/Attribute/Processor/Element/TransformerAttributeProcessor.php diff --git a/src/Processor/GenerateConfiguratorStrategy.php b/src/Attribute/Processor/GenerateConfiguratorStrategy.php similarity index 100% rename from src/Processor/GenerateConfiguratorStrategy.php rename to src/Attribute/Processor/GenerateConfiguratorStrategy.php diff --git a/src/Processor/MethodChildBuilderAttributeInterface.php b/src/Attribute/Processor/MethodChildBuilderAttributeInterface.php similarity index 100% rename from src/Processor/MethodChildBuilderAttributeInterface.php rename to src/Attribute/Processor/MethodChildBuilderAttributeInterface.php diff --git a/src/Processor/PostConfigureInterface.php b/src/Attribute/Processor/PostConfigureInterface.php similarity index 100% rename from src/Processor/PostConfigureInterface.php rename to src/Attribute/Processor/PostConfigureInterface.php diff --git a/src/Processor/PostConfigureReflectionSetProperties.php b/src/Attribute/Processor/PostConfigureReflectionSetProperties.php similarity index 100% rename from src/Processor/PostConfigureReflectionSetProperties.php rename to src/Attribute/Processor/PostConfigureReflectionSetProperties.php diff --git a/src/Processor/ProcessorMetadata.php b/src/Attribute/Processor/ProcessorMetadata.php similarity index 100% rename from src/Processor/ProcessorMetadata.php rename to src/Attribute/Processor/ProcessorMetadata.php diff --git a/src/Processor/ReflectionProcessor.php b/src/Attribute/Processor/ReflectionProcessor.php similarity index 100% rename from src/Processor/ReflectionProcessor.php rename to src/Attribute/Processor/ReflectionProcessor.php diff --git a/src/Processor/ReflectionStrategyInterface.php b/src/Attribute/Processor/ReflectionStrategyInterface.php similarity index 100% rename from src/Processor/ReflectionStrategyInterface.php rename to src/Attribute/Processor/ReflectionStrategyInterface.php diff --git a/tests/Aggregate/ArrayConstraintTest.php b/tests/Attribute/Aggregate/ArrayConstraintTest.php similarity index 100% rename from tests/Aggregate/ArrayConstraintTest.php rename to tests/Attribute/Aggregate/ArrayConstraintTest.php diff --git a/tests/Aggregate/ArrayTransformerTest.php b/tests/Attribute/Aggregate/ArrayTransformerTest.php similarity index 100% rename from tests/Aggregate/ArrayTransformerTest.php rename to tests/Attribute/Aggregate/ArrayTransformerTest.php diff --git a/tests/Aggregate/AsArrayConstraintTest.php b/tests/Attribute/Aggregate/AsArrayConstraintTest.php similarity index 100% rename from tests/Aggregate/AsArrayConstraintTest.php rename to tests/Attribute/Aggregate/AsArrayConstraintTest.php diff --git a/tests/Aggregate/CallbackArrayConstraintTest.php b/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php similarity index 100% rename from tests/Aggregate/CallbackArrayConstraintTest.php rename to tests/Attribute/Aggregate/CallbackArrayConstraintTest.php diff --git a/tests/Aggregate/CountTest.php b/tests/Attribute/Aggregate/CountTest.php similarity index 100% rename from tests/Aggregate/CountTest.php rename to tests/Attribute/Aggregate/CountTest.php diff --git a/tests/Aggregate/ElementTypeTest.php b/tests/Attribute/Aggregate/ElementTypeTest.php similarity index 100% rename from tests/Aggregate/ElementTypeTest.php rename to tests/Attribute/Aggregate/ElementTypeTest.php diff --git a/tests/Button/GroupsTest.php b/tests/Attribute/Button/GroupsTest.php similarity index 100% rename from tests/Button/GroupsTest.php rename to tests/Attribute/Button/GroupsTest.php diff --git a/tests/Button/ValueTest.php b/tests/Attribute/Button/ValueTest.php similarity index 100% rename from tests/Button/ValueTest.php rename to tests/Attribute/Button/ValueTest.php diff --git a/tests/Child/AsFilterTest.php b/tests/Attribute/Child/AsFilterTest.php similarity index 100% rename from tests/Child/AsFilterTest.php rename to tests/Attribute/Child/AsFilterTest.php diff --git a/tests/Child/AsModelTransformerTest.php b/tests/Attribute/Child/AsModelTransformerTest.php similarity index 100% rename from tests/Child/AsModelTransformerTest.php rename to tests/Attribute/Child/AsModelTransformerTest.php diff --git a/tests/Child/CallbackFilterTest.php b/tests/Attribute/Child/CallbackFilterTest.php similarity index 100% rename from tests/Child/CallbackFilterTest.php rename to tests/Attribute/Child/CallbackFilterTest.php diff --git a/tests/Child/CallbackModelTransformerTest.php b/tests/Attribute/Child/CallbackModelTransformerTest.php similarity index 100% rename from tests/Child/CallbackModelTransformerTest.php rename to tests/Attribute/Child/CallbackModelTransformerTest.php diff --git a/tests/Child/ConfigureTest.php b/tests/Attribute/Child/ConfigureTest.php similarity index 100% rename from tests/Child/ConfigureTest.php rename to tests/Attribute/Child/ConfigureTest.php diff --git a/tests/Child/DefaultValueTest.php b/tests/Attribute/Child/DefaultValueTest.php similarity index 100% rename from tests/Child/DefaultValueTest.php rename to tests/Attribute/Child/DefaultValueTest.php diff --git a/tests/Child/DependenciesTest.php b/tests/Attribute/Child/DependenciesTest.php similarity index 100% rename from tests/Child/DependenciesTest.php rename to tests/Attribute/Child/DependenciesTest.php diff --git a/tests/Child/GetSetTest.php b/tests/Attribute/Child/GetSetTest.php similarity index 100% rename from tests/Child/GetSetTest.php rename to tests/Attribute/Child/GetSetTest.php diff --git a/tests/Child/HttpFieldTest.php b/tests/Attribute/Child/HttpFieldTest.php similarity index 100% rename from tests/Child/HttpFieldTest.php rename to tests/Attribute/Child/HttpFieldTest.php diff --git a/tests/Child/ModelTransformerTest.php b/tests/Attribute/Child/ModelTransformerTest.php similarity index 100% rename from tests/Child/ModelTransformerTest.php rename to tests/Attribute/Child/ModelTransformerTest.php diff --git a/tests/Constraint/AsConstraintTest.php b/tests/Attribute/Constraint/AsConstraintTest.php similarity index 100% rename from tests/Constraint/AsConstraintTest.php rename to tests/Attribute/Constraint/AsConstraintTest.php diff --git a/tests/Constraint/CallbackConstraintTest.php b/tests/Attribute/Constraint/CallbackConstraintTest.php similarity index 100% rename from tests/Constraint/CallbackConstraintTest.php rename to tests/Attribute/Constraint/CallbackConstraintTest.php diff --git a/tests/Constraint/SatisfyTest.php b/tests/Attribute/Constraint/SatisfyTest.php similarity index 100% rename from tests/Constraint/SatisfyTest.php rename to tests/Attribute/Constraint/SatisfyTest.php diff --git a/tests/Element/AsTransformerTest.php b/tests/Attribute/Element/AsTransformerTest.php similarity index 100% rename from tests/Element/AsTransformerTest.php rename to tests/Attribute/Element/AsTransformerTest.php diff --git a/tests/Element/CallbackTransformerTest.php b/tests/Attribute/Element/CallbackTransformerTest.php similarity index 100% rename from tests/Element/CallbackTransformerTest.php rename to tests/Attribute/Element/CallbackTransformerTest.php diff --git a/tests/Element/ChoicesTest.php b/tests/Attribute/Element/ChoicesTest.php similarity index 100% rename from tests/Element/ChoicesTest.php rename to tests/Attribute/Element/ChoicesTest.php diff --git a/tests/Element/Date/AfterFieldTest.php b/tests/Attribute/Element/Date/AfterFieldTest.php similarity index 100% rename from tests/Element/Date/AfterFieldTest.php rename to tests/Attribute/Element/Date/AfterFieldTest.php diff --git a/tests/Element/Date/BeforeFieldTest.php b/tests/Attribute/Element/Date/BeforeFieldTest.php similarity index 100% rename from tests/Element/Date/BeforeFieldTest.php rename to tests/Attribute/Element/Date/BeforeFieldTest.php diff --git a/tests/Element/Date/DateFormatTest.php b/tests/Attribute/Element/Date/DateFormatTest.php similarity index 100% rename from tests/Element/Date/DateFormatTest.php rename to tests/Attribute/Element/Date/DateFormatTest.php diff --git a/tests/Element/Date/DateTimeClassTest.php b/tests/Attribute/Element/Date/DateTimeClassTest.php similarity index 100% rename from tests/Element/Date/DateTimeClassTest.php rename to tests/Attribute/Element/Date/DateTimeClassTest.php diff --git a/tests/Element/Date/ImmtableDateTimeTest.php b/tests/Attribute/Element/Date/ImmtableDateTimeTest.php similarity index 100% rename from tests/Element/Date/ImmtableDateTimeTest.php rename to tests/Attribute/Element/Date/ImmtableDateTimeTest.php diff --git a/tests/Element/Date/TimezoneTest.php b/tests/Attribute/Element/Date/TimezoneTest.php similarity index 100% rename from tests/Element/Date/TimezoneTest.php rename to tests/Attribute/Element/Date/TimezoneTest.php diff --git a/tests/Element/IgnoreTransformerExceptionTest.php b/tests/Attribute/Element/IgnoreTransformerExceptionTest.php similarity index 100% rename from tests/Element/IgnoreTransformerExceptionTest.php rename to tests/Attribute/Element/IgnoreTransformerExceptionTest.php diff --git a/tests/Element/RawTest.php b/tests/Attribute/Element/RawTest.php similarity index 100% rename from tests/Element/RawTest.php rename to tests/Attribute/Element/RawTest.php diff --git a/tests/Element/RequiredTest.php b/tests/Attribute/Element/RequiredTest.php similarity index 100% rename from tests/Element/RequiredTest.php rename to tests/Attribute/Element/RequiredTest.php diff --git a/tests/Element/TransformerErrorTest.php b/tests/Attribute/Element/TransformerErrorTest.php similarity index 100% rename from tests/Element/TransformerErrorTest.php rename to tests/Attribute/Element/TransformerErrorTest.php diff --git a/tests/Element/TransformerTest.php b/tests/Attribute/Element/TransformerTest.php similarity index 100% rename from tests/Element/TransformerTest.php rename to tests/Attribute/Element/TransformerTest.php diff --git a/tests/Form/CallbackGeneratorTest.php b/tests/Attribute/Form/CallbackGeneratorTest.php similarity index 100% rename from tests/Form/CallbackGeneratorTest.php rename to tests/Attribute/Form/CallbackGeneratorTest.php diff --git a/tests/Form/CsrfTest.php b/tests/Attribute/Form/CsrfTest.php similarity index 100% rename from tests/Form/CsrfTest.php rename to tests/Attribute/Form/CsrfTest.php diff --git a/tests/Form/GeneratesTest.php b/tests/Attribute/Form/GeneratesTest.php similarity index 100% rename from tests/Form/GeneratesTest.php rename to tests/Attribute/Form/GeneratesTest.php diff --git a/tests/FunctionalTest.php b/tests/Attribute/FunctionalTest.php similarity index 100% rename from tests/FunctionalTest.php rename to tests/Attribute/FunctionalTest.php diff --git a/tests/Php81/Aggregate/ArrayConstraintTest.php b/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php similarity index 100% rename from tests/Php81/Aggregate/ArrayConstraintTest.php rename to tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php diff --git a/tests/Php81/Aggregate/CallbackArrayConstraintTest.php b/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php similarity index 100% rename from tests/Php81/Aggregate/CallbackArrayConstraintTest.php rename to tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php diff --git a/tests/Php81/Constraint/SatisfyTest.php b/tests/Attribute/Php81/Constraint/SatisfyTest.php similarity index 100% rename from tests/Php81/Constraint/SatisfyTest.php rename to tests/Attribute/Php81/Constraint/SatisfyTest.php diff --git a/tests/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php b/tests/Attribute/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php similarity index 100% rename from tests/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php rename to tests/Attribute/Processor/CodeGenerator/AttributesProcessorGeneratorTest.php diff --git a/tests/Processor/CodeGenerator/ClassGeneratorTest.php b/tests/Attribute/Processor/CodeGenerator/ClassGeneratorTest.php similarity index 100% rename from tests/Processor/CodeGenerator/ClassGeneratorTest.php rename to tests/Attribute/Processor/CodeGenerator/ClassGeneratorTest.php diff --git a/tests/Processor/CodeGenerator/ObjectInstantiationTest.php b/tests/Attribute/Processor/CodeGenerator/ObjectInstantiationTest.php similarity index 100% rename from tests/Processor/CodeGenerator/ObjectInstantiationTest.php rename to tests/Attribute/Processor/CodeGenerator/ObjectInstantiationTest.php diff --git a/tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php b/tests/Attribute/Processor/CodeGenerator/TransformerClassGeneratorTest.php similarity index 100% rename from tests/Processor/CodeGenerator/TransformerClassGeneratorTest.php rename to tests/Attribute/Processor/CodeGenerator/TransformerClassGeneratorTest.php diff --git a/tests/Processor/CompileAttributesProcessorTest.php b/tests/Attribute/Processor/CompileAttributesProcessorTest.php similarity index 100% rename from tests/Processor/CompileAttributesProcessorTest.php rename to tests/Attribute/Processor/CompileAttributesProcessorTest.php diff --git a/tests/Processor/Element/ConstraintAttributeProcessorTest.php b/tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php similarity index 100% rename from tests/Processor/Element/ConstraintAttributeProcessorTest.php rename to tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php diff --git a/tests/Processor/Element/ExtractorAttributeProcessorTest.php b/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php similarity index 100% rename from tests/Processor/Element/ExtractorAttributeProcessorTest.php rename to tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php diff --git a/tests/Processor/Element/FilterAttributeProcessorTest.php b/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php similarity index 100% rename from tests/Processor/Element/FilterAttributeProcessorTest.php rename to tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php diff --git a/tests/Processor/Element/HydratorAttributeProcessorTest.php b/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php similarity index 100% rename from tests/Processor/Element/HydratorAttributeProcessorTest.php rename to tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php diff --git a/tests/Processor/Element/TransformerAttributeProcessorTest.php b/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php similarity index 100% rename from tests/Processor/Element/TransformerAttributeProcessorTest.php rename to tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php diff --git a/tests/Processor/GenerateConfiguratorStrategyTest.php b/tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php similarity index 100% rename from tests/Processor/GenerateConfiguratorStrategyTest.php rename to tests/Attribute/Processor/GenerateConfiguratorStrategyTest.php diff --git a/tests/Processor/ReflectionProcessorTest.php b/tests/Attribute/Processor/ReflectionProcessorTest.php similarity index 100% rename from tests/Processor/ReflectionProcessorTest.php rename to tests/Attribute/Processor/ReflectionProcessorTest.php diff --git a/tests/TestCase.php b/tests/Attribute/TestCase.php similarity index 100% rename from tests/TestCase.php rename to tests/Attribute/TestCase.php From cf85d6dd056d5fa24bd5303e877fd4e1ac2818b4 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 18 Mar 2026 12:24:34 +0100 Subject: [PATCH 36/40] chore: remove file that conflict with bdf-form repository --- .github/workflows/php.yml | 78 --------------------------------------- .scrutinizer.yml | 20 ---------- LICENSE | 19 ---------- infection.json | 14 ------- phpunit.xml.dist | 26 ------------- psalm.xml | 17 --------- 6 files changed, 174 deletions(-) delete mode 100644 .github/workflows/php.yml delete mode 100644 .scrutinizer.yml delete mode 100644 LICENSE delete mode 100644 infection.json delete mode 100755 phpunit.xml.dist delete mode 100644 psalm.xml diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml deleted file mode 100644 index f47ffcf..0000000 --- a/.github/workflows/php.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: CI - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] - name: PHP ${{ matrix.php-versions }} - - steps: - - uses: actions/checkout@v2 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: json - ini-values: date.timezone=Europe/Paris - - name: Check PHP Version - run: php -v - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" - # Docs: https://getcomposer.org/doc/articles/scripts.md - - - name: Run test suite - run: composer run-script tests - - analysis: - name: Analysis - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.0 - extensions: json - ini-values: date.timezone=Europe/Paris - - - name: Install Infection - run: | - wget https://github.com/infection/infection/releases/download/0.25.6/infection.phar - wget https://github.com/infection/infection/releases/download/0.25.6/infection.phar.asc - chmod +x infection.phar - gpg --recv-keys C6D76C329EBADE2FB9C458CFC5095986493B4AA0 - gpg --with-fingerprint --verify infection.phar.asc infection.phar - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run type coverage - run: composer run-script psalm - - - name: Run check style - run: composer run-script phpcs - - - name: Run Infection - run: | - git fetch --depth=1 origin $GITHUB_BASE_REF - XDEBUG_MODE=coverage ./infection.phar --logger-github --git-diff-filter=AM diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 78bb2c0..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,20 +0,0 @@ -build: - environment: - php: 8.0.2 - - nodes: - analysis: - tests: - override: - - php-scrutinizer-run - coverage: - tests: - override: - - command: XDEBUG_MODE=coverage composer run tests-with-coverage - coverage: - file: coverage.xml - format: clover - -filter: - excluded_paths: - - 'tests/*' diff --git a/LICENSE b/LICENSE deleted file mode 100644 index abad63b..0000000 --- a/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2004-2021 B2PWeb - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/infection.json b/infection.json deleted file mode 100644 index 7af8d9d..0000000 --- a/infection.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/infection/infection/0.25.3/resources/schema.json", - "source": { - "directories": [ - "src" - ] - }, - "logs": { - "text": "infection.log" - }, - "mutators": { - "@default": true - } -} diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100755 index a9e870d..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - ./src - - - - - - - - ./tests - - - diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 6d7bfee..0000000 --- a/psalm.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - From d6dbef52274e6848c1071ab62eb269a1ace50686 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 18 Mar 2026 16:56:22 +0100 Subject: [PATCH 37/40] fix(attribute): Compatibility with form v2 --- composer.json | 3 +- src/Attribute/Aggregate/ArrayConstraint.php | 36 ++++-------------- src/Attribute/Aggregate/AsArrayConstraint.php | 2 +- .../Aggregate/CallbackArrayConstraint.php | 15 +++----- src/Attribute/Constraint/AsConstraint.php | 5 +-- .../Constraint/CallbackConstraint.php | 15 +++----- src/Attribute/Constraint/Satisfy.php | 38 ++++--------------- src/Attribute/Element/AsTransformer.php | 2 +- src/Attribute/Element/CallbackTransformer.php | 9 ++--- src/Attribute/Element/Choices.php | 17 ++++----- .../CodeGenerator/ClassGenerator.php | 6 +++ .../CodeGenerator/ObjectInstantiation.php | 37 ++++++++++-------- .../ConfigureFormBuilderStrategy.php | 1 - .../Processor/ReflectionProcessor.php | 1 - .../Aggregate/ArrayConstraintTest.php | 11 +++--- .../Aggregate/ArrayTransformerTest.php | 5 +-- .../Aggregate/AsArrayConstraintTest.php | 9 ++--- .../Aggregate/CallbackArrayConstraintTest.php | 9 ++--- tests/Attribute/Aggregate/CountTest.php | 5 +-- tests/Attribute/Aggregate/ElementTypeTest.php | 13 ++----- tests/Attribute/Button/GroupsTest.php | 5 +-- tests/Attribute/Button/ValueTest.php | 5 +-- tests/Attribute/Child/AsFilterTest.php | 5 +-- .../Child/AsModelTransformerTest.php | 5 +-- tests/Attribute/Child/CallbackFilterTest.php | 5 +-- .../Child/CallbackModelTransformerTest.php | 17 ++++----- tests/Attribute/Child/ConfigureTest.php | 15 +++----- tests/Attribute/Child/DefaultValueTest.php | 5 +-- tests/Attribute/Child/DependenciesTest.php | 5 +-- tests/Attribute/Child/GetSetTest.php | 5 +-- tests/Attribute/Child/HttpFieldTest.php | 5 +-- .../Attribute/Child/ModelTransformerTest.php | 5 +-- .../Attribute/Constraint/AsConstraintTest.php | 7 ++-- .../Constraint/CallbackConstraintTest.php | 7 ++-- tests/Attribute/Constraint/SatisfyTest.php | 11 +++--- tests/Attribute/Element/AsTransformerTest.php | 5 +-- .../Element/CallbackTransformerTest.php | 14 +++---- tests/Attribute/Element/ChoicesTest.php | 11 +++--- .../Attribute/Element/Date/AfterFieldTest.php | 15 +++----- .../Element/Date/BeforeFieldTest.php | 15 +++----- .../Attribute/Element/Date/DateFormatTest.php | 5 +-- .../Element/Date/DateTimeClassTest.php | 7 ++-- .../Element/Date/ImmtableDateTimeTest.php | 7 ++-- tests/Attribute/Element/Date/TimezoneTest.php | 5 +-- .../IgnoreTransformerExceptionTest.php | 5 +-- tests/Attribute/Element/RawTest.php | 6 +-- tests/Attribute/Element/RequiredTest.php | 6 +-- .../Element/TransformerErrorTest.php | 10 ++--- tests/Attribute/Element/TransformerTest.php | 10 ++--- .../Attribute/Form/CallbackGeneratorTest.php | 5 +-- tests/Attribute/Form/CsrfTest.php | 25 +++++------- tests/Attribute/Form/GeneratesTest.php | 5 +-- tests/Attribute/FunctionalTest.php | 35 +++++++---------- .../Php81/Aggregate/ArrayConstraintTest.php | 27 +++---------- .../Aggregate/CallbackArrayConstraintTest.php | 9 ++--- .../Php81/Constraint/SatisfyTest.php | 11 +++--- .../CodeGenerator/ObjectInstantiationTest.php | 14 +++---- .../TransformerClassGeneratorTest.php | 12 +++--- .../CompileAttributesProcessorTest.php | 8 ++-- .../ConstraintAttributeProcessorTest.php | 5 +-- .../ExtractorAttributeProcessorTest.php | 5 +-- .../Element/FilterAttributeProcessorTest.php | 5 +-- .../HydratorAttributeProcessorTest.php | 5 +-- .../TransformerAttributeProcessorTest.php | 5 +-- .../Processor/ReflectionProcessorTest.php | 13 ++++--- tests/Attribute/TestCase.php | 2 +- 66 files changed, 263 insertions(+), 395 deletions(-) diff --git a/composer.json b/composer.json index 32221a1..8a5c545 100755 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ }, "autoload-dev": { "psr-4": { - "Bdf\\Form\\": "tests" + "Bdf\\Form\\": "tests", + "Tests\\Form\\Attribute\\": "tests/Attribute" } }, "require": { diff --git a/src/Attribute/Aggregate/ArrayConstraint.php b/src/Attribute/Aggregate/ArrayConstraint.php index f6117fd..6f6c5df 100644 --- a/src/Attribute/Aggregate/ArrayConstraint.php +++ b/src/Attribute/Aggregate/ArrayConstraint.php @@ -10,13 +10,8 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation; use Bdf\Form\Child\ChildBuilderInterface; -use InvalidArgumentException; -use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; -use function is_object; -use function is_string; - /** * Add a constraint on the whole array element * Use Satisfy, or directly the constraint as attribute for add a constraint on one array item @@ -47,7 +42,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class ArrayConstraint implements ChildBuilderAttributeInterface +final readonly class ArrayConstraint implements ChildBuilderAttributeInterface { public function __construct( /** @@ -60,29 +55,17 @@ public function __construct( * the constructor parameters will be deduced from public properties of the constraint. * This may not work if the constraint has a complex constructor. * - * @var class-string|Constraint - * @readonly + * @var Constraint */ - private string|Constraint $constraint, - /** - * Constraint's constructor options - * - * @var mixed|null - * @readonly - */ - private mixed $options = null - ) { - if (is_object($constraint) && $options !== null) { - throw new InvalidArgumentException('Cannot use options with constraint instance'); - } - } + private Constraint $constraint, + ) {} /** * {@inheritdoc} */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - $builder->arrayConstraint($this->constraint, $this->options); + $builder->arrayConstraint($this->constraint); } /** @@ -90,12 +73,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - if (is_string($this->constraint)) { - $constraint = $generator->useAndSimplifyType($this->constraint); - $generator->line('$?->arrayConstraint(?::class, ?);', [$name, new Literal($constraint), $this->options]); - } else { - $constraint = ObjectInstantiation::singleArrayParameter($this->constraint)->render($generator); - $generator->line('$?->arrayConstraint(?);', [$name, $constraint]); - } + $constraint = ObjectInstantiation::promotedProperties($this->constraint)->render($generator); + $generator->line('$?->arrayConstraint(?);', [$name, $constraint]); } } diff --git a/src/Attribute/Aggregate/AsArrayConstraint.php b/src/Attribute/Aggregate/AsArrayConstraint.php index fc9e90c..43f376c 100644 --- a/src/Attribute/Aggregate/AsArrayConstraint.php +++ b/src/Attribute/Aggregate/AsArrayConstraint.php @@ -51,7 +51,7 @@ * @api */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -final class AsArrayConstraint implements MethodChildBuilderAttributeInterface +final readonly class AsArrayConstraint implements MethodChildBuilderAttributeInterface { public function __construct( /** diff --git a/src/Attribute/Aggregate/CallbackArrayConstraint.php b/src/Attribute/Aggregate/CallbackArrayConstraint.php index bb83539..fcae412 100644 --- a/src/Attribute/Aggregate/CallbackArrayConstraint.php +++ b/src/Attribute/Aggregate/CallbackArrayConstraint.php @@ -44,7 +44,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CallbackArrayConstraint implements ChildBuilderAttributeInterface +final readonly class CallbackArrayConstraint implements ChildBuilderAttributeInterface { public function __construct( /** @@ -72,19 +72,14 @@ public function __construct( * @readonly */ private ?string $message = null, - ) { - } + ) {} /** * {@inheritdoc} */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - $constraint = new Closure(['callback' => [$form, $this->methodName]]); - - if ($this->message !== null) { - $constraint->message = $this->message; - } + $constraint = new Closure($form->{$this->methodName}(...), $this->message); $builder->arrayConstraint($constraint); } @@ -97,8 +92,8 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen $generator->use(Closure::class, 'ClosureConstraint'); $parameters = $this->message !== null - ? new Literal("['callback' => [\$form, ?], 'message' => ?]", [$this->methodName, $this->message]) - : new Literal('[$form, ?]', [$this->methodName]) + ? new Literal('$form->?(...), ?', [$this->methodName, $this->message]) + : new Literal('$form->?(...)', [$this->methodName]) ; $generator->line('$?->arrayConstraint(new ClosureConstraint(?));', [$name, $parameters]); diff --git a/src/Attribute/Constraint/AsConstraint.php b/src/Attribute/Constraint/AsConstraint.php index ef05d98..b2a02ff 100644 --- a/src/Attribute/Constraint/AsConstraint.php +++ b/src/Attribute/Constraint/AsConstraint.php @@ -51,7 +51,7 @@ * @api */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -final class AsConstraint implements MethodChildBuilderAttributeInterface +final readonly class AsConstraint implements MethodChildBuilderAttributeInterface { public function __construct( /** @@ -70,8 +70,7 @@ public function __construct( * @readonly */ private ?string $message = null, - ) { - } + ) {} /** * {@inheritdoc} diff --git a/src/Attribute/Constraint/CallbackConstraint.php b/src/Attribute/Constraint/CallbackConstraint.php index 249bea6..2f49f4b 100644 --- a/src/Attribute/Constraint/CallbackConstraint.php +++ b/src/Attribute/Constraint/CallbackConstraint.php @@ -47,7 +47,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CallbackConstraint implements ChildBuilderAttributeInterface +final readonly class CallbackConstraint implements ChildBuilderAttributeInterface { public function __construct( /** @@ -75,19 +75,14 @@ public function __construct( * @readonly */ private ?string $message = null, - ) { - } + ) {} /** * {@inheritdoc} */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - $constraint = new Closure(['callback' => [$form, $this->methodName]]); - - if ($this->message !== null) { - $constraint->message = $this->message; - } + $constraint = new Closure($form->{$this->methodName}(...), $this->message); $builder->satisfy($constraint); } @@ -100,8 +95,8 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen $generator->use(Closure::class, 'ClosureConstraint'); $parameters = $this->message !== null - ? new Literal("['callback' => [\$form, ?], 'message' => ?]", [$this->methodName, $this->message]) - : new Literal('[$form, ?]', [$this->methodName]) + ? new Literal('$form->?(...), ?', [$this->methodName, $this->message]) + : new Literal('$form->?(...)', [$this->methodName]) ; $generator->line('$?->satisfy(new ClosureConstraint(?));', [$name, $parameters]); diff --git a/src/Attribute/Constraint/Satisfy.php b/src/Attribute/Constraint/Satisfy.php index 83c7ab6..b4599c9 100644 --- a/src/Attribute/Constraint/Satisfy.php +++ b/src/Attribute/Constraint/Satisfy.php @@ -8,11 +8,9 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation; use Bdf\Form\Child\ChildBuilderInterface; -use InvalidArgumentException; use Nette\PhpGenerator\Literal; use Symfony\Component\Validator\Constraint; -use function is_object; use function is_string; /** @@ -46,41 +44,26 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -class Satisfy implements ChildBuilderAttributeInterface +final readonly class Satisfy implements ChildBuilderAttributeInterface { public function __construct( /** * The constraint * - * You can use a class name, and provider arguments on the next parameter, - * or directly use the constraint instance. - * - * When a constraint instance is used, in case of code generation, - * the constructor parameters will be deduced from public properties of the constraint. + * The constructor parameters will be deduced from public properties of the constraint. * This may not work if the constraint has a complex constructor. - * @var class-string|Constraint - * @readonly - */ - private string|Constraint $constraint, - /** - * Constraint's constructor options * - * @var array|null|string - * @readonly + * @var Constraint */ - private mixed $options = null - ) { - if (is_object($constraint) && $options !== null) { - throw new InvalidArgumentException('Cannot use options with constraint instance'); - } - } + private Constraint $constraint, + ) {} /** * {@inheritdoc} */ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { - $builder->satisfy($this->constraint, $this->options); + $builder->satisfy($this->constraint); } /** @@ -88,12 +71,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ */ public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { - if (is_string($this->constraint)) { - $type = $generator->useAndSimplifyType($this->constraint); - $generator->line('$?->satisfy(?::class, ?);', [$name, new Literal($type), $this->options]); - } else { - $constraint = ObjectInstantiation::singleArrayParameter($this->constraint)->render($generator); - $generator->line('$?->satisfy(?);', [$name, $constraint]); - } + $constraint = ObjectInstantiation::promotedProperties($this->constraint)->render($generator); + $generator->line('$?->satisfy(?);', [$name, $constraint]); } } diff --git a/src/Attribute/Element/AsTransformer.php b/src/Attribute/Element/AsTransformer.php index aea8c06..f26c0b1 100644 --- a/src/Attribute/Element/AsTransformer.php +++ b/src/Attribute/Element/AsTransformer.php @@ -47,7 +47,7 @@ * @api */ #[Attribute(Attribute::TARGET_METHOD)] -final class AsTransformer implements MethodChildBuilderAttributeInterface +final readonly class AsTransformer implements MethodChildBuilderAttributeInterface { /** * @var list diff --git a/src/Attribute/Element/CallbackTransformer.php b/src/Attribute/Element/CallbackTransformer.php index 310b166..0824fed 100644 --- a/src/Attribute/Element/CallbackTransformer.php +++ b/src/Attribute/Element/CallbackTransformer.php @@ -77,7 +77,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CallbackTransformer implements ChildBuilderAttributeInterface +final readonly class CallbackTransformer implements ChildBuilderAttributeInterface { public function __construct( /** @@ -102,8 +102,7 @@ public function __construct( * @readonly */ private ?string $toHttp = null, - ) { - } + ) {} /** * {@inheritdoc} @@ -126,7 +125,7 @@ public function __construct( /** * {@inheritdoc} */ - public function transformToHttp($value, ElementInterface $input) + public function transformToHttp(mixed $value, ElementInterface $input): mixed { if ($this->toHttp === null) { return $value; @@ -138,7 +137,7 @@ public function transformToHttp($value, ElementInterface $input) /** * {@inheritdoc} */ - public function transformFromHttp($value, ElementInterface $input) + public function transformFromHttp(mixed $value, ElementInterface $input): mixed { if ($this->fromHttp === null) { return $value; diff --git a/src/Attribute/Element/Choices.php b/src/Attribute/Element/Choices.php index 83c6936..3ea63d8 100644 --- a/src/Attribute/Element/Choices.php +++ b/src/Attribute/Element/Choices.php @@ -70,7 +70,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Choices implements ChildBuilderAttributeInterface +final readonly class Choices implements ChildBuilderAttributeInterface { public function __construct( /** @@ -101,8 +101,7 @@ public function __construct( * @readonly */ private array $options = [], - ) { - } + ) {} /** * {@inheritdoc} @@ -112,14 +111,14 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $options = $this->options; if ($this->message !== null) { - $options['message'] = $options['multipleMessage'] = $this->message; + $options['message'] = $this->message; } // 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 + is_string($this->choices) ? new LazyChoice($form->{$this->choices}(...)) : $this->choices, + ...$options ); } @@ -131,16 +130,16 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen $options = $this->options; if ($this->message !== null) { - $options['message'] = $options['multipleMessage'] = $this->message; + $options['message'] = $this->message; } if (is_string($this->choices)) { $generator->use(LazyChoice::class); - $choices = new Literal('new LazyChoice([$form, ?])', [$this->choices]); + $choices = new Literal('new LazyChoice($form->?(...))', [$this->choices]); } else { $choices = $this->choices; } - $generator->line('$?->choices(?, ?);', [$name, $choices, $options]); + $generator->line('$?->choices(?, ...?:);', [$name, $choices, $options]); } } diff --git a/src/Attribute/Processor/CodeGenerator/ClassGenerator.php b/src/Attribute/Processor/CodeGenerator/ClassGenerator.php index d00e0ff..521c0c1 100644 --- a/src/Attribute/Processor/CodeGenerator/ClassGenerator.php +++ b/src/Attribute/Processor/CodeGenerator/ClassGenerator.php @@ -92,6 +92,12 @@ final public function implementsMethod(string $interface, string $methodName): M $this->namespace->addUse($returnType->getSingleName()); } + if ($returnType !== null) { + $method->setReturnType((string) $returnType); + } else { + $method->setReturnType('mixed'); + } + $this->class->addMember($method); return $method; diff --git a/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php b/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php index 9d1630c..c2941c4 100644 --- a/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php +++ b/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php @@ -10,7 +10,7 @@ /** * Utility class for generate an object instantiation */ -final class ObjectInstantiation +final readonly class ObjectInstantiation { public function __construct( /** @@ -27,8 +27,7 @@ public function __construct( * @readonly */ private array $constructorParameters = [], - ) { - } + ) {} /** * Render the object instantiation @@ -46,8 +45,7 @@ public function render(?ClassGenerator $generator = null): Literal /** * Configure the ObjectInstantiation utility to generate - * the constructor call with a single array parameter, - * use to inject all public properties of an object. + * the constructor call using promoted properties of the given object. * * This method should be used for symfony constraints. * Properties with default value will be ignored. @@ -55,11 +53,11 @@ public function render(?ClassGenerator $generator = null): Literal * @param object $object * @return self */ - public static function singleArrayParameter(object $object): self + public static function promotedProperties(object $object): self { return new self( get_class($object), - [self::extractPublicProperties($object)] + self::extractPromotedProperties($object), ); } @@ -71,22 +69,29 @@ public static function singleArrayParameter(object $object): self * @return array * @psalm-suppress MixedAssignment */ - private static function extractPublicProperties(object $object): array + private static function extractPromotedProperties(object $object): array { $reflectionObject = new ReflectionObject($object); - $properties = []; + $parameters = []; + + foreach ($reflectionObject->getConstructor()?->getParameters() ?? [] as $param) { + $value = $object->{$param->name} ?? null; - foreach ($reflectionObject->getProperties() as $property) { - if ($property->isPublic() && !$property->isStatic()) { - $property->setAccessible(true); - $value = $property->getValue($object); + if ($param->isDefaultValueAvailable() && $value === $param->getDefaultValue()) { + continue; + } - if ($property->getDefaultValue() !== $value) { - $properties[$property->getName()] = $value; + if ($reflectionObject->hasProperty($param->name)) { + $property = $reflectionObject->getProperty($param->name); + + if ($property->hasDefaultValue() && $value === $property->getDefaultValue()) { + continue; } } + + $parameters[$param->name] = $value; } - return $properties; + return $parameters; } } diff --git a/src/Attribute/Processor/ConfigureFormBuilderStrategy.php b/src/Attribute/Processor/ConfigureFormBuilderStrategy.php index 62aece7..721ad2d 100644 --- a/src/Attribute/Processor/ConfigureFormBuilderStrategy.php +++ b/src/Attribute/Processor/ConfigureFormBuilderStrategy.php @@ -69,7 +69,6 @@ public function onElementProperty(ReflectionProperty $property, string $name, st $attributeInstance = $attribute->newInstance(); if ($attributeInstance instanceof ChildBuilderAttributeInterface) { - /** @var ChildBuilderAttributeInterface $attributeInstance */ $attributeInstance->applyOnChildBuilder($form, $elementBuilder); continue; } diff --git a/src/Attribute/Processor/ReflectionProcessor.php b/src/Attribute/Processor/ReflectionProcessor.php index caedcfd..ec0c88f 100644 --- a/src/Attribute/Processor/ReflectionProcessor.php +++ b/src/Attribute/Processor/ReflectionProcessor.php @@ -61,7 +61,6 @@ public function configureBuilder(AttributeForm $form, FormBuilderInterface $buil } $elementType = $property->getType()->getName(); - PHP_VERSION_ID >= 80100 or $property->setAccessible(true); if ($elementType === ButtonInterface::class) { $metadata->addButtonProperty($name, $property); diff --git a/tests/Attribute/Aggregate/ArrayConstraintTest.php b/tests/Attribute/Aggregate/ArrayConstraintTest.php index 4ef93d9..335ae09 100644 --- a/tests/Attribute/Aggregate/ArrayConstraintTest.php +++ b/tests/Attribute/Aggregate/ArrayConstraintTest.php @@ -9,18 +9,17 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Attribute\Processor\ReflectionProcessor; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Validator\Constraints\Unique; use Tests\Form\Attribute\TestCase; class ArrayConstraintTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { - #[ArrayConstraint(Unique::class, ['message' => 'Not unique'])] + #[ArrayConstraint(new Unique(message: 'Not unique'))] public ArrayElement $values; }; @@ -38,7 +37,7 @@ public function test(AttributesProcessorInterface $processor) public function test_code_generator() { $form = new class extends AttributeForm { - #[ArrayConstraint(Unique::class, ['message' => 'Not unique'])] + #[ArrayConstraint(new Unique(message: 'Not unique'))] public ArrayElement $values; }; @@ -61,7 +60,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $values = $builder->add('values', ArrayElement::class); - $values->arrayConstraint(Unique::class, ['message' => 'Not unique']); + $values->arrayConstraint(new Unique(message: 'Not unique', groups: ['Default'])); return $this; } diff --git a/tests/Attribute/Aggregate/ArrayTransformerTest.php b/tests/Attribute/Aggregate/ArrayTransformerTest.php index 0f7c0d6..7ada9f3 100644 --- a/tests/Attribute/Aggregate/ArrayTransformerTest.php +++ b/tests/Attribute/Aggregate/ArrayTransformerTest.php @@ -8,13 +8,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\ElementInterface; use Bdf\Form\Transformer\TransformerInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ArrayTransformerTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Aggregate/AsArrayConstraintTest.php b/tests/Attribute/Aggregate/AsArrayConstraintTest.php index baaae23..3a41782 100644 --- a/tests/Attribute/Aggregate/AsArrayConstraintTest.php +++ b/tests/Attribute/Aggregate/AsArrayConstraintTest.php @@ -7,13 +7,12 @@ use Bdf\Form\Attribute\Aggregate\CallbackArrayConstraint; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class AsArrayConstraintTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -83,10 +82,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', ArrayElement::class); - $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2'])); + $foo->arrayConstraint(new ClosureConstraint($form->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($form->validateFoo(...))); return $this; } diff --git a/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php b/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php index 678b9c1..77bc0ae 100644 --- a/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php +++ b/tests/Attribute/Aggregate/CallbackArrayConstraintTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Aggregate\CallbackArrayConstraint; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CallbackArrayConstraintTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -83,10 +82,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', ArrayElement::class); - $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2'])); + $foo->arrayConstraint(new ClosureConstraint($form->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($form->validateFoo(...))); return $this; } diff --git a/tests/Attribute/Aggregate/CountTest.php b/tests/Attribute/Aggregate/CountTest.php index 378ad4c..25946d1 100644 --- a/tests/Attribute/Aggregate/CountTest.php +++ b/tests/Attribute/Aggregate/CountTest.php @@ -9,13 +9,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Attribute\Processor\ReflectionProcessor; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CountTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Aggregate/ElementTypeTest.php b/tests/Attribute/Aggregate/ElementTypeTest.php index 87b87f9..97bda4c 100644 --- a/tests/Attribute/Aggregate/ElementTypeTest.php +++ b/tests/Attribute/Aggregate/ElementTypeTest.php @@ -14,13 +14,12 @@ use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ElementTypeTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test_simple(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -34,9 +33,7 @@ public function test_simple(AttributesProcessorInterface $processor) $this->assertSame(['values' => [123, 456, 789]], $form->value()); } - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test_with_configurator(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -55,9 +52,7 @@ public function configureField(IntegerElementBuilder $builder): void $this->assertEquals(['values' => [0 => 'This value should be greater than or equal to 200.']], $form->error()->toArray()); } - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test_with_embedded(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Button/GroupsTest.php b/tests/Attribute/Button/GroupsTest.php index f2cf2e7..49dba86 100644 --- a/tests/Attribute/Button/GroupsTest.php +++ b/tests/Attribute/Button/GroupsTest.php @@ -9,13 +9,12 @@ use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Button\ButtonInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class GroupsTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Button/ValueTest.php b/tests/Attribute/Button/ValueTest.php index 0d52422..884d280 100644 --- a/tests/Attribute/Button/ValueTest.php +++ b/tests/Attribute/Button/ValueTest.php @@ -10,13 +10,12 @@ use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Button\ButtonInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ValueTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/AsFilterTest.php b/tests/Attribute/Child/AsFilterTest.php index 4471fad..bf02108 100644 --- a/tests/Attribute/Child/AsFilterTest.php +++ b/tests/Attribute/Child/AsFilterTest.php @@ -10,13 +10,12 @@ use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class AsFilterTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/AsModelTransformerTest.php b/tests/Attribute/Child/AsModelTransformerTest.php index 53a292d..a6cd85c 100644 --- a/tests/Attribute/Child/AsModelTransformerTest.php +++ b/tests/Attribute/Child/AsModelTransformerTest.php @@ -10,13 +10,12 @@ use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class AsModelTransformerTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new #[Generates(Struct::class)] class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/CallbackFilterTest.php b/tests/Attribute/Child/CallbackFilterTest.php index fe82523..5fda5d1 100644 --- a/tests/Attribute/Child/CallbackFilterTest.php +++ b/tests/Attribute/Child/CallbackFilterTest.php @@ -9,13 +9,12 @@ use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CallbackFilterTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/CallbackModelTransformerTest.php b/tests/Attribute/Child/CallbackModelTransformerTest.php index 742ff66..2dd7d48 100644 --- a/tests/Attribute/Child/CallbackModelTransformerTest.php +++ b/tests/Attribute/Child/CallbackModelTransformerTest.php @@ -10,13 +10,12 @@ use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CallbackModelTransformerTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new #[Generates(Struct::class)] class(null, $processor) extends AttributeForm { @@ -49,9 +48,7 @@ public function bToInput($value, IntegerElement $input) $this->assertEquals(10, $form->b->value()); } - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test_with_only_one_transformation_method(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -120,7 +117,7 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? /** * {@inheritdoc} */ - function transformToHttp($value, ElementInterface $input) + function transformToHttp(mixed $value, ElementInterface $input): mixed { return $value; } @@ -128,7 +125,7 @@ function transformToHttp($value, ElementInterface $input) /** * {@inheritdoc} */ - function transformFromHttp($value, ElementInterface $input) + function transformFromHttp(mixed $value, ElementInterface $input): mixed { return $this->form->t($value, $input); } @@ -146,7 +143,7 @@ public function __construct( /** * {@inheritdoc} */ - function transformToHttp($value, ElementInterface $input) + function transformToHttp(mixed $value, ElementInterface $input): mixed { return $this->form->t($value, $input); } @@ -154,7 +151,7 @@ function transformToHttp($value, ElementInterface $input) /** * {@inheritdoc} */ - function transformFromHttp($value, ElementInterface $input) + function transformFromHttp(mixed $value, ElementInterface $input): mixed { return $value; } diff --git a/tests/Attribute/Child/ConfigureTest.php b/tests/Attribute/Child/ConfigureTest.php index aea2064..502f942 100644 --- a/tests/Attribute/Child/ConfigureTest.php +++ b/tests/Attribute/Child/ConfigureTest.php @@ -8,13 +8,12 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\Leaf\StringElementBuilder; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ConfigureTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -26,7 +25,7 @@ public function test(AttributesProcessorInterface $processor) */ public function configureFoo(ChildBuilderInterface $builder): void { - $builder->length(['min' => 3]); + $builder->length(min: 3); } }; @@ -38,9 +37,7 @@ public function configureFoo(ChildBuilderInterface $builder): void $this->assertTrue($form->valid()); } - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test_on_method(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -52,7 +49,7 @@ public function test_on_method(AttributesProcessorInterface $processor) #[Configure('foo')] public function configureFoo(ChildBuilderInterface $builder): void { - $builder->length(['min' => 3]); + $builder->length(min: 3); } }; @@ -75,7 +72,7 @@ public function test_code_generator() */ public function configureFoo(ChildBuilderInterface $builder): void { - $builder->length(['min' => 3]); + $builder->length(min: 3); } }; diff --git a/tests/Attribute/Child/DefaultValueTest.php b/tests/Attribute/Child/DefaultValueTest.php index a25ec79..2bced97 100644 --- a/tests/Attribute/Child/DefaultValueTest.php +++ b/tests/Attribute/Child/DefaultValueTest.php @@ -13,13 +13,12 @@ use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class DefaultValueTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/DependenciesTest.php b/tests/Attribute/Child/DependenciesTest.php index e2b7982..407010b 100644 --- a/tests/Attribute/Child/DependenciesTest.php +++ b/tests/Attribute/Child/DependenciesTest.php @@ -15,13 +15,12 @@ use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class DependenciesTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/GetSetTest.php b/tests/Attribute/Child/GetSetTest.php index bf483e7..68b106b 100644 --- a/tests/Attribute/Child/GetSetTest.php +++ b/tests/Attribute/Child/GetSetTest.php @@ -14,13 +14,12 @@ use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class GetSetTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/HttpFieldTest.php b/tests/Attribute/Child/HttpFieldTest.php index 41ffe08..2a3884a 100644 --- a/tests/Attribute/Child/HttpFieldTest.php +++ b/tests/Attribute/Child/HttpFieldTest.php @@ -14,13 +14,12 @@ use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\IntegerElementBuilder; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class HttpFieldTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Child/ModelTransformerTest.php b/tests/Attribute/Child/ModelTransformerTest.php index 59031d7..1917aff 100644 --- a/tests/Attribute/Child/ModelTransformerTest.php +++ b/tests/Attribute/Child/ModelTransformerTest.php @@ -13,13 +13,12 @@ use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; use Bdf\Form\Transformer\TransformerInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ModelTransformerTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new #[Generates(Struct::class)] class extends AttributeForm { diff --git a/tests/Attribute/Constraint/AsConstraintTest.php b/tests/Attribute/Constraint/AsConstraintTest.php index 6677a39..389e8a9 100644 --- a/tests/Attribute/Constraint/AsConstraintTest.php +++ b/tests/Attribute/Constraint/AsConstraintTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Constraint\AsConstraint; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class AsConstraintTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -79,7 +78,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', StringElement::class); - $foo->satisfy(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo length must be a multiple of 2'])); + $foo->satisfy(new ClosureConstraint($form->validateFoo(...), 'Foo length must be a multiple of 2')); return $this; } diff --git a/tests/Attribute/Constraint/CallbackConstraintTest.php b/tests/Attribute/Constraint/CallbackConstraintTest.php index 536a2fb..ab44e4c 100644 --- a/tests/Attribute/Constraint/CallbackConstraintTest.php +++ b/tests/Attribute/Constraint/CallbackConstraintTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Constraint\CallbackConstraint; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CallbackConstraintTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -80,7 +79,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', StringElement::class); - $foo->satisfy(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo length must be a multiple of 2'])); + $foo->satisfy(new ClosureConstraint($form->validateFoo(...), 'Foo length must be a multiple of 2')); return $this; } diff --git a/tests/Attribute/Constraint/SatisfyTest.php b/tests/Attribute/Constraint/SatisfyTest.php index 0c15fda..66255fa 100644 --- a/tests/Attribute/Constraint/SatisfyTest.php +++ b/tests/Attribute/Constraint/SatisfyTest.php @@ -6,18 +6,17 @@ use Bdf\Form\Attribute\Constraint\Satisfy; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\Length; class SatisfyTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { - #[Satisfy(Length::class, ['min' => 3])] + #[Satisfy(new Length(min: 3))] public StringElement $foo; }; @@ -32,7 +31,7 @@ public function test(AttributesProcessorInterface $processor) public function test_code_generator() { $form = new class extends AttributeForm { - #[Satisfy(Length::class, ['min' => 3])] + #[Satisfy(new Length(min: 3))] public StringElement $foo; }; @@ -55,7 +54,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', StringElement::class); - $foo->satisfy(Length::class, ['min' => 3]); + $foo->satisfy(new Length(min: 3, groups: ['Default'])); return $this; } diff --git a/tests/Attribute/Element/AsTransformerTest.php b/tests/Attribute/Element/AsTransformerTest.php index c6161bf..3dfb156 100644 --- a/tests/Attribute/Element/AsTransformerTest.php +++ b/tests/Attribute/Element/AsTransformerTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Element\AsTransformer; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class AsTransformerTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/CallbackTransformerTest.php b/tests/Attribute/Element/CallbackTransformerTest.php index 2ff5c9d..04c5c12 100644 --- a/tests/Attribute/Element/CallbackTransformerTest.php +++ b/tests/Attribute/Element/CallbackTransformerTest.php @@ -8,13 +8,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\IntegerElement; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CallbackTransformerTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -51,9 +50,8 @@ public function outTransformer($value, StringElement $input) $this->assertEquals('["out","[\"in\",\"b\"]"]', $view['bar']->value()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_with_only_one_transformation_method(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -132,7 +130,7 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? /** * {@inheritdoc} */ - function transformToHttp($value, ElementInterface $input) + function transformToHttp(mixed $value, ElementInterface $input): mixed { return $this->form->outTransformer($value, $input); } @@ -140,7 +138,7 @@ function transformToHttp($value, ElementInterface $input) /** * {@inheritdoc} */ - function transformFromHttp($value, ElementInterface $input) + function transformFromHttp(mixed $value, ElementInterface $input): mixed { return $this->form->inTransformer($value, $input); } diff --git a/tests/Attribute/Element/ChoicesTest.php b/tests/Attribute/Element/ChoicesTest.php index bbd68ab..aa393a7 100644 --- a/tests/Attribute/Element/ChoicesTest.php +++ b/tests/Attribute/Element/ChoicesTest.php @@ -8,13 +8,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Choice\ArrayChoice; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ChoicesTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -85,13 +84,13 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', StringElement::class); - $foo->choices(['foo', 'bar'], ['multipleMessage' => 'my error', 'message' => 'my error']); + $foo->choices(['foo', 'bar'], message: 'my error'); $bar = $builder->add('bar', ArrayElement::class); - $bar->choices(['foo', 'bar', 'baz'], ['min' => 2, 'multipleMessage' => 'my error', 'message' => 'my error']); + $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($form->generateChoices(...)), ); return $this; } diff --git a/tests/Attribute/Element/Date/AfterFieldTest.php b/tests/Attribute/Element/Date/AfterFieldTest.php index 34e6be7..6db6219 100644 --- a/tests/Attribute/Element/Date/AfterFieldTest.php +++ b/tests/Attribute/Element/Date/AfterFieldTest.php @@ -7,13 +7,12 @@ use Bdf\Form\Attribute\Element\Date\AfterField; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\Date\DateTimeElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class AfterFieldTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -40,9 +39,8 @@ public function test(AttributesProcessorInterface $processor) ], self::normalizeSpace($form->error()->toArray())); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_with_message(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -69,9 +67,8 @@ public function test_with_message(AttributesProcessorInterface $processor) ], $form->error()->toArray()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_with_or_equal(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/Date/BeforeFieldTest.php b/tests/Attribute/Element/Date/BeforeFieldTest.php index 180ba86..d56ba6d 100644 --- a/tests/Attribute/Element/Date/BeforeFieldTest.php +++ b/tests/Attribute/Element/Date/BeforeFieldTest.php @@ -8,13 +8,12 @@ use Bdf\Form\Attribute\Element\Date\DateFormat; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\Date\DateTimeElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class BeforeFieldTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -41,9 +40,8 @@ public function test(AttributesProcessorInterface $processor) ], self::normalizeSpace($form->error()->toArray())); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_with_message(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -70,9 +68,8 @@ public function test_with_message(AttributesProcessorInterface $processor) ], $form->error()->toArray()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_with_or_equal(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/Date/DateFormatTest.php b/tests/Attribute/Element/Date/DateFormatTest.php index 58c1eaf..6a0d565 100644 --- a/tests/Attribute/Element/Date/DateFormatTest.php +++ b/tests/Attribute/Element/Date/DateFormatTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Element\Date\DateFormat; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\Date\DateTimeElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class DateFormatTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/Date/DateTimeClassTest.php b/tests/Attribute/Element/Date/DateTimeClassTest.php index aea5039..0f8d6c9 100644 --- a/tests/Attribute/Element/Date/DateTimeClassTest.php +++ b/tests/Attribute/Element/Date/DateTimeClassTest.php @@ -8,13 +8,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\Date\DateTimeElement; use Bdf\Form\Leaf\FloatElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class DateTimeClassTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -22,7 +21,7 @@ public function test(AttributesProcessorInterface $processor) public DateTimeElement $foo; }; - $form->submit(['foo' => '2020-11-02T15:21:31+0000']); + $form->submit(['foo' => '2020-11-02T15:21:31+0100']); $this->assertEquals(new MyCustomDate('2020-11-02T15:21:31'), $form->foo->value()); $this->assertInstanceOf(MyCustomDate::class, $form->foo->value()); diff --git a/tests/Attribute/Element/Date/ImmtableDateTimeTest.php b/tests/Attribute/Element/Date/ImmtableDateTimeTest.php index 4b38689..09b3b58 100644 --- a/tests/Attribute/Element/Date/ImmtableDateTimeTest.php +++ b/tests/Attribute/Element/Date/ImmtableDateTimeTest.php @@ -10,13 +10,12 @@ use Bdf\Form\Leaf\Date\DateTimeElement; use Bdf\Form\Leaf\FloatElement; use DateTimeImmutable; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ImmtableDateTimeTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -24,7 +23,7 @@ public function test(AttributesProcessorInterface $processor) public DateTimeElement $foo; }; - $form->submit(['foo' => '2020-11-02T15:21:31+0000']); + $form->submit(['foo' => '2020-11-02T15:21:31+0100']); $this->assertEquals(new DateTimeImmutable('2020-11-02T15:21:31'), $form->foo->value()); $this->assertInstanceOf(DateTimeImmutable::class, $form->foo->value()); diff --git a/tests/Attribute/Element/Date/TimezoneTest.php b/tests/Attribute/Element/Date/TimezoneTest.php index ed4ddb9..18b0af0 100644 --- a/tests/Attribute/Element/Date/TimezoneTest.php +++ b/tests/Attribute/Element/Date/TimezoneTest.php @@ -8,13 +8,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\Date\DateTimeElement; use DateTime; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class TimezoneTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/IgnoreTransformerExceptionTest.php b/tests/Attribute/Element/IgnoreTransformerExceptionTest.php index 2f2ab6d..24a1e54 100644 --- a/tests/Attribute/Element/IgnoreTransformerExceptionTest.php +++ b/tests/Attribute/Element/IgnoreTransformerExceptionTest.php @@ -7,13 +7,12 @@ use Bdf\Form\Attribute\Element\IgnoreTransformerException; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class IgnoreTransformerExceptionTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/RawTest.php b/tests/Attribute/Element/RawTest.php index 5a223c4..33f3e9a 100644 --- a/tests/Attribute/Element/RawTest.php +++ b/tests/Attribute/Element/RawTest.php @@ -6,6 +6,7 @@ use Bdf\Form\Attribute\Element\Raw; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\FloatElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class RawTest extends TestCase @@ -23,9 +24,8 @@ protected function tearDown(): void \Locale::setDefault($this->lastLocale); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/RequiredTest.php b/tests/Attribute/Element/RequiredTest.php index 6df30e7..002614f 100644 --- a/tests/Attribute/Element/RequiredTest.php +++ b/tests/Attribute/Element/RequiredTest.php @@ -6,13 +6,13 @@ use Bdf\Form\Attribute\Element\Required; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\FloatElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class RequiredTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/TransformerErrorTest.php b/tests/Attribute/Element/TransformerErrorTest.php index 31e8511..df91419 100644 --- a/tests/Attribute/Element/TransformerErrorTest.php +++ b/tests/Attribute/Element/TransformerErrorTest.php @@ -9,13 +9,12 @@ use Bdf\Form\Leaf\StringElement; use Bdf\Form\Validator\TransformerExceptionConstraint; use http\Message; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class TransformerErrorTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -36,9 +35,8 @@ public function transformer() $this->assertEquals('BAR_ERROR', $form->error()->children()['bar']->code()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_with_callback(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Element/TransformerTest.php b/tests/Attribute/Element/TransformerTest.php index 8f24d2e..db220de 100644 --- a/tests/Attribute/Element/TransformerTest.php +++ b/tests/Attribute/Element/TransformerTest.php @@ -9,13 +9,12 @@ use Bdf\Form\ElementInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\Transformer\TransformerInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class TransformerTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -30,9 +29,8 @@ public function test(AttributesProcessorInterface $processor) $this->assertEquals('A_A', $view['foo']->value()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function testWithArray(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Form/CallbackGeneratorTest.php b/tests/Attribute/Form/CallbackGeneratorTest.php index 092761d..9611a84 100644 --- a/tests/Attribute/Form/CallbackGeneratorTest.php +++ b/tests/Attribute/Form/CallbackGeneratorTest.php @@ -11,13 +11,12 @@ use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CallbackGeneratorTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new #[CallbackGenerator('generate')] class extends AttributeForm { diff --git a/tests/Attribute/Form/CsrfTest.php b/tests/Attribute/Form/CsrfTest.php index 1f03add..083441f 100644 --- a/tests/Attribute/Form/CsrfTest.php +++ b/tests/Attribute/Form/CsrfTest.php @@ -6,14 +6,13 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Form\Csrf; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Security\Csrf\CsrfTokenManager; use Tests\Form\Attribute\TestCase; class CsrfTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new #[Csrf] class(null, $processor) extends AttributeForm { @@ -27,9 +26,8 @@ public function test(AttributesProcessorInterface $processor) $this->assertTrue($form->valid()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_message(AttributesProcessorInterface $processor) { $form = new #[Csrf(message: 'my error')] class(null, $processor) extends AttributeForm { @@ -43,9 +41,8 @@ public function test_message(AttributesProcessorInterface $processor) $this->assertTrue($form->valid()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_name(AttributesProcessorInterface $processor) { $form = new #[Csrf(name: 't')] class(null, $processor) extends AttributeForm { @@ -59,9 +56,8 @@ public function test_name(AttributesProcessorInterface $processor) $this->assertTrue($form->valid()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_invalidate(AttributesProcessorInterface $processor) { $form = new #[Csrf(invalidate: true)] class(null, $processor) extends AttributeForm { @@ -76,9 +72,8 @@ public function test_invalidate(AttributesProcessorInterface $processor) $this->assertFalse($form->valid()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_tokenId(AttributesProcessorInterface $processor) { $form = new #[Csrf(tokenId: 'my_token_id')] class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Form/GeneratesTest.php b/tests/Attribute/Form/GeneratesTest.php index 78adc47..a731fa9 100644 --- a/tests/Attribute/Form/GeneratesTest.php +++ b/tests/Attribute/Form/GeneratesTest.php @@ -15,15 +15,14 @@ use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\NotEqualTo; use Symfony\Component\Validator\Constraints\Positive; class GeneratesTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new #[Generates(Person::class)] class extends AttributeForm { diff --git a/tests/Attribute/FunctionalTest.php b/tests/Attribute/FunctionalTest.php index e84898f..449963d 100644 --- a/tests/Attribute/FunctionalTest.php +++ b/tests/Attribute/FunctionalTest.php @@ -16,6 +16,7 @@ use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; use Bdf\Form\Transformer\TransformerInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\GreaterThan; use Symfony\Component\Validator\Constraints\Length; @@ -23,9 +24,7 @@ class FunctionalTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test_simple(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -65,9 +64,8 @@ public function test_simple(AttributesProcessorInterface $processor) ], $form->error()->toArray()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_setter_with_name(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -79,9 +77,8 @@ public function test_setter_with_name(AttributesProcessorInterface $processor) $this->assertSame(['bar' => 'azerty'], $form->value()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_getter_with_name(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -93,9 +90,8 @@ public function test_getter_with_name(AttributesProcessorInterface $processor) $this->assertSame('aqw', $form->foo->value()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_inheritance(AttributesProcessorInterface $processor) { $form = new ChildForm(null, $processor); @@ -242,9 +238,8 @@ function postConfigure(AttributeForm $form, FormInterface $inner): void ); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_buttons(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -271,9 +266,8 @@ public function test_buttons(AttributesProcessorInterface $processor) $this->assertTrue($form->bar->clicked()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_filter(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -285,9 +279,8 @@ public function test_filter(AttributesProcessorInterface $processor) $this->assertSame(['foo' => '<hello>&world'], $form->value()); } - /** - * @dataProvider provideAttributesProcessor - */ + + #[DataProvider('provideAttributesProcessor')] public function test_transformer(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php b/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php index 67c44c5..6865919 100644 --- a/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php +++ b/tests/Attribute/Php81/Aggregate/ArrayConstraintTest.php @@ -9,18 +9,17 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Attribute\Processor\ReflectionProcessor; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Validator\Constraints\Unique; use Tests\Form\Attribute\TestCase; class ArrayConstraintTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { - #[ArrayConstraint(new Unique(['message' => 'Not unique']))] + #[ArrayConstraint(new Unique(message: 'Not unique'))] public ArrayElement $values; }; @@ -32,29 +31,13 @@ public function test(AttributesProcessorInterface $processor) $this->assertTrue($form->valid()); } - /** - * @dataProvider provideAttributesProcessor - */ - public function test_disallow_constraint_instance_with_option_arg(AttributesProcessorInterface $processor) - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot use options with constraint instance'); - - $form = new class(null, $processor) extends AttributeForm { - #[ArrayConstraint(new Unique(['message' => 'Not unique']), ['foo' => 'bar'])] - public ArrayElement $values; - }; - - $form->submit(['values' => ['aaa', 'aaa']]); - } - /** * @return void */ public function test_code_generator() { $form = new class extends AttributeForm { - #[ArrayConstraint(new Unique(['message' => 'Not unique']))] + #[ArrayConstraint(new Unique(message: 'Not unique'))] public ArrayElement $values; }; @@ -77,7 +60,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $values = $builder->add('values', ArrayElement::class); - $values->arrayConstraint(new Unique(['message' => 'Not unique', 'groups' => ['Default']])); + $values->arrayConstraint(new Unique(message: 'Not unique', groups: ['Default'])); return $this; } diff --git a/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php b/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php index 7c64ffc..96dae0f 100644 --- a/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php +++ b/tests/Attribute/Php81/Aggregate/CallbackArrayConstraintTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Aggregate\CallbackArrayConstraint; use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class CallbackArrayConstraintTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { @@ -83,10 +82,10 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', ArrayElement::class); - $foo->arrayConstraint(new ClosureConstraint(['callback' => [$form, 'validateFoo'], 'message' => 'Foo size must be a multiple of 2'])); + $foo->arrayConstraint(new ClosureConstraint($form->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($form->validateFoo(...))); return $this; } diff --git a/tests/Attribute/Php81/Constraint/SatisfyTest.php b/tests/Attribute/Php81/Constraint/SatisfyTest.php index 61d8011..05585f0 100644 --- a/tests/Attribute/Php81/Constraint/SatisfyTest.php +++ b/tests/Attribute/Php81/Constraint/SatisfyTest.php @@ -6,18 +6,17 @@ use Bdf\Form\Attribute\Constraint\Satisfy; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\Length; class SatisfyTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { - #[Satisfy(new Length(['min' => 3]))] + #[Satisfy(new Length(min: 3))] public StringElement $foo; }; @@ -32,7 +31,7 @@ public function test(AttributesProcessorInterface $processor) public function test_code_generator() { $form = new class extends AttributeForm { - #[Satisfy(new Length(['min' => 3]))] + #[Satisfy(new Length(min: 3))] public StringElement $foo; }; @@ -55,7 +54,7 @@ class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigu function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $foo = $builder->add('foo', StringElement::class); - $foo->satisfy(new Length(['min' => 3, 'groups' => ['Default']])); + $foo->satisfy(new Length(min: 3, groups: ['Default'])); return $this; } diff --git a/tests/Attribute/Processor/CodeGenerator/ObjectInstantiationTest.php b/tests/Attribute/Processor/CodeGenerator/ObjectInstantiationTest.php index 8d1c6be..96b147f 100644 --- a/tests/Attribute/Processor/CodeGenerator/ObjectInstantiationTest.php +++ b/tests/Attribute/Processor/CodeGenerator/ObjectInstantiationTest.php @@ -16,25 +16,25 @@ public function test_default() { $o = new NotBlank(); - $this->assertEquals("new Symfony\Component\Validator\Constraints\NotBlank(['groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render()); + $this->assertEquals("new Symfony\Component\Validator\Constraints\NotBlank(groups: ['Default'])", (string) ObjectInstantiation::promotedProperties($o)->render()); - $this->assertEquals($o, eval('return ' . ObjectInstantiation::singleArrayParameter($o)->render().';')); + $this->assertEquals($o, eval('return ' . ObjectInstantiation::promotedProperties($o)->render().';')); } public function test_with_parameters() { - $o = new Range(['min' => 1, 'max' => 10]); + $o = new Range(min: 1, max: 10); - $this->assertEquals("new Symfony\Component\Validator\Constraints\Range(['min' => 1, 'max' => 10, 'groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render()); + $this->assertEquals("new Symfony\Component\Validator\Constraints\Range(min: 1, max: 10, groups: ['Default'])", (string) ObjectInstantiation::promotedProperties($o)->render()); - $this->assertEquals($o, eval('return ' . ObjectInstantiation::singleArrayParameter($o)->render().';')); + $this->assertEquals($o, eval('return ' . ObjectInstantiation::promotedProperties($o)->render().';')); } public function test_with_simplified_class_name() { - $o = new Range(['min' => 1, 'max' => 10]); + $o = new Range(min: 1, max: 10); $generator = new ClassGenerator(new PhpNamespace('Foo'), new ClassType('Bar')); - $this->assertEquals("new Range(['min' => 1, 'max' => 10, 'groups' => ['Default']])", ObjectInstantiation::singleArrayParameter($o)->render($generator)); + $this->assertEquals("new Range(min: 1, max: 10, groups: ['Default'])", (string) ObjectInstantiation::promotedProperties($o)->render($generator)); } } diff --git a/tests/Attribute/Processor/CodeGenerator/TransformerClassGeneratorTest.php b/tests/Attribute/Processor/CodeGenerator/TransformerClassGeneratorTest.php index 9a544d4..364e19e 100644 --- a/tests/Attribute/Processor/CodeGenerator/TransformerClassGeneratorTest.php +++ b/tests/Attribute/Processor/CodeGenerator/TransformerClassGeneratorTest.php @@ -21,14 +21,14 @@ public function test_empty() /** * {@inheritdoc} */ - function transformToHttp($value, ElementInterface $input) + function transformToHttp(mixed $value, ElementInterface $input): mixed { } /** * {@inheritdoc} */ - function transformFromHttp($value, ElementInterface $input) + function transformFromHttp(mixed $value, ElementInterface $input): mixed { } } @@ -61,14 +61,14 @@ public function test_withPromotedProperty() /** * {@inheritdoc} */ - function transformToHttp($value, ElementInterface $input) + function transformToHttp(mixed $value, ElementInterface $input): mixed { } /** * {@inheritdoc} */ - function transformFromHttp($value, ElementInterface $input) + function transformFromHttp(mixed $value, ElementInterface $input): mixed { } @@ -108,7 +108,7 @@ public function test_with_methods_body() /** * {@inheritdoc} */ - function transformToHttp($value, ElementInterface $input) + function transformToHttp(mixed $value, ElementInterface $input): mixed { return $value + 2; } @@ -116,7 +116,7 @@ function transformToHttp($value, ElementInterface $input) /** * {@inheritdoc} */ - function transformFromHttp($value, ElementInterface $input) + function transformFromHttp(mixed $value, ElementInterface $input): mixed { return $value - 2; } diff --git a/tests/Attribute/Processor/CompileAttributesProcessorTest.php b/tests/Attribute/Processor/CompileAttributesProcessorTest.php index 9df1e3f..39feed4 100644 --- a/tests/Attribute/Processor/CompileAttributesProcessorTest.php +++ b/tests/Attribute/Processor/CompileAttributesProcessorTest.php @@ -73,11 +73,11 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $firstName = $builder->add('firstName', StringElement::class); $firstName->satisfy(new NotBlank()); - $firstName->satisfy(new ClosureConstraint([$form, 'validateName'])); + $firstName->satisfy(new ClosureConstraint($form->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($form->validateName(...))); $lastName->hydrator(new Setter(null))->extractor(new Getter(null)); $age = $builder->add('age', IntegerElement::class); @@ -274,11 +274,11 @@ function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ? $firstName = $builder->add('firstName', StringElement::class); $firstName->satisfy(new NotBlank()); - $firstName->satisfy(new ClosureConstraint([$form, 'validateName'])); + $firstName->satisfy(new ClosureConstraint($form->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($form->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 0fa4812..18de5f9 100644 --- a/tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php +++ b/tests/Attribute/Processor/Element/ConstraintAttributeProcessorTest.php @@ -5,15 +5,14 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotEqualTo; class ConstraintAttributeProcessorTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php b/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php index 4bf2dd3..516078f 100644 --- a/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php +++ b/tests/Attribute/Processor/Element/ExtractorAttributeProcessorTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class ExtractorAttributeProcessorTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php b/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php index dfae091..88674a8 100644 --- a/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php +++ b/tests/Attribute/Processor/Element/FilterAttributeProcessorTest.php @@ -9,13 +9,12 @@ use Bdf\Form\Filter\FilterInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Getter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class FilterAttributeProcessorTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php b/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php index 96b09f4..3301a52 100644 --- a/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php +++ b/tests/Attribute/Processor/Element/HydratorAttributeProcessorTest.php @@ -6,13 +6,12 @@ use Bdf\Form\Attribute\Processor\AttributesProcessorInterface; use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class HydratorAttributeProcessorTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php b/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php index ad09f11..9390485 100644 --- a/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php +++ b/tests/Attribute/Processor/Element/TransformerAttributeProcessorTest.php @@ -9,13 +9,12 @@ use Bdf\Form\Leaf\StringElement; use Bdf\Form\PropertyAccess\Setter; use Bdf\Form\Transformer\TransformerInterface; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\Form\Attribute\TestCase; class TransformerAttributeProcessorTest extends TestCase { - /** - * @dataProvider provideAttributesProcessor - */ + #[DataProvider('provideAttributesProcessor')] public function test(AttributesProcessorInterface $processor) { $form = new class(null, $processor) extends AttributeForm { diff --git a/tests/Attribute/Processor/ReflectionProcessorTest.php b/tests/Attribute/Processor/ReflectionProcessorTest.php index ec75444..70f6b12 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\ProcessorMetadata; use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Attribute\Processor\ReflectionStrategyInterface; use Bdf\Form\Button\ButtonInterface; @@ -20,12 +21,12 @@ public function test_should_iterate_hierarchy() $form = new B(); $builder = new FormBuilder(); - $strategy->expects($this->exactly(2))->method('onFormClass') - ->withConsecutive( - [new \ReflectionClass(B::class), $form, $builder], - [new \ReflectionClass(A::class), $form, $builder], - ) - ; + $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), + }; + }); $processor->configureBuilder($form, $builder); } diff --git a/tests/Attribute/TestCase.php b/tests/Attribute/TestCase.php index 4fe0fca..c18deeb 100644 --- a/tests/Attribute/TestCase.php +++ b/tests/Attribute/TestCase.php @@ -15,7 +15,7 @@ class TestCase extends \PHPUnit\Framework\TestCase /** * @return AttributesProcessorInterface[] */ - public function provideAttributesProcessor(): array + public static function provideAttributesProcessor(): array { return [ 'reflection' => [new ReflectionProcessor(new ConfigureFormBuilderStrategy())], From 30c2fede80f6257a21b2293117b127261e7106cf Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 18 Mar 2026 17:11:16 +0100 Subject: [PATCH 38/40] fix(attribute): SF 6 constraint compatibility --- .../Processor/CodeGenerator/ObjectInstantiation.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php b/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php index c2941c4..c51ac85 100644 --- a/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php +++ b/src/Attribute/Processor/CodeGenerator/ObjectInstantiation.php @@ -4,6 +4,7 @@ use Nette\PhpGenerator\Literal; use ReflectionObject; +use Symfony\Component\Validator\Constraint; use function get_class; @@ -77,6 +78,11 @@ private static function extractPromotedProperties(object $object): array foreach ($reflectionObject->getConstructor()?->getParameters() ?? [] as $param) { $value = $object->{$param->name} ?? null; + // Compatibility with SF 6: ignore options parameter + if ($param->name === 'options' && $object instanceof Constraint) { + continue; + } + if ($param->isDefaultValueAvailable() && $value === $param->getDefaultValue()) { continue; } From 5dc191e615ba3c6e32b4574baca5891e08318b87 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 18 Mar 2026 17:53:01 +0100 Subject: [PATCH 39/40] ci(attribute): fix psalm errors --- composer.json | 2 +- src/Attribute/Aggregate/ArrayConstraint.php | 9 +++---- src/Attribute/Aggregate/AsArrayConstraint.php | 9 +++---- .../Aggregate/CallbackArrayConstraint.php | 9 +++---- src/Attribute/Aggregate/Count.php | 13 +++------- src/Attribute/Aggregate/ElementType.php | 9 +++---- src/Attribute/AttributeForm.php | 11 +++----- src/Attribute/Button/Groups.php | 9 +++---- src/Attribute/Button/Value.php | 14 ++++------ src/Attribute/Child/AsFilter.php | 11 +++----- src/Attribute/Child/AsModelTransformer.php | 11 +++----- src/Attribute/Child/CallbackFilter.php | 14 ++++------ .../Child/CallbackModelTransformer.php | 26 +++++++------------ src/Attribute/Child/Configure.php | 22 +++++----------- src/Attribute/Child/DefaultValue.php | 11 +++----- src/Attribute/Child/Dependencies.php | 11 +++----- src/Attribute/Child/GetSet.php | 14 ++++------ src/Attribute/Child/HttpField.php | 14 ++++------ src/Attribute/Child/ModelTransformer.php | 11 +++----- src/Attribute/Constraint/AsConstraint.php | 10 +++---- .../Constraint/CallbackConstraint.php | 9 +++---- src/Attribute/Constraint/Satisfy.php | 9 +++---- src/Attribute/Element/AsTransformer.php | 9 +++---- src/Attribute/Element/CallbackTransformer.php | 17 ++++-------- src/Attribute/Element/Choices.php | 9 +++---- src/Attribute/Element/Date/AfterField.php | 14 ++++------ src/Attribute/Element/Date/BeforeField.php | 14 ++++------ src/Attribute/Element/Date/DateFormat.php | 14 ++++------ src/Attribute/Element/Date/DateTimeClass.php | 14 ++++------ .../Element/Date/ImmutableDateTime.php | 11 +++----- src/Attribute/Element/Date/Timezone.php | 14 ++++------ .../Element/IgnoreTransformerException.php | 14 ++++------ src/Attribute/Element/Raw.php | 14 ++++------ src/Attribute/Element/Required.php | 14 ++++------ src/Attribute/Element/Transformer.php | 18 +++++-------- src/Attribute/Element/TransformerError.php | 14 ++++------ src/Attribute/Form/CallbackGenerator.php | 14 ++++------ src/Attribute/Form/Csrf.php | 14 ++++------ src/Attribute/Form/Generates.php | 14 ++++------ .../Processor/CompileAttributesProcessor.php | 11 ++++---- .../ConfigureFormBuilderStrategy.php | 17 ++++-------- .../Element/ConstraintAttributeProcessor.php | 13 +++------- .../Element/ExtractorAttributeProcessor.php | 13 +++------- .../Element/FilterAttributeProcessor.php | 13 +++------- .../Element/HydratorAttributeProcessor.php | 13 +++------- .../Element/TransformerAttributeProcessor.php | 13 +++------- .../GenerateConfiguratorStrategy.php | 17 ++++-------- .../PostConfigureReflectionSetProperties.php | 10 +++---- .../Processor/ReflectionProcessor.php | 9 +++---- 49 files changed, 216 insertions(+), 403 deletions(-) diff --git a/composer.json b/composer.json index 8a5c545..ff529c0 100755 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "symfony/security-csrf": "~6.4|~7.0|~8.0", "giggsey/libphonenumber-for-php": "~8.0|~9.0", "phpunit/phpunit": "~13.0", - "vimeo/psalm": "~6.15.1", + "vimeo/psalm": "^6.16.0", "symfony/http-foundation": "~6.4|~7.0|~8.0", "symfony/form": "~6.4|~7.0|~8.0" }, diff --git a/src/Attribute/Aggregate/ArrayConstraint.php b/src/Attribute/Aggregate/ArrayConstraint.php index 6f6c5df..c179498 100644 --- a/src/Attribute/Aggregate/ArrayConstraint.php +++ b/src/Attribute/Aggregate/ArrayConstraint.php @@ -10,6 +10,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation; use Bdf\Form\Child\ChildBuilderInterface; +use Override; use Symfony\Component\Validator\Constraint; /** @@ -60,17 +61,13 @@ public function __construct( private Constraint $constraint, ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->arrayConstraint($this->constraint); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $constraint = ObjectInstantiation::promotedProperties($this->constraint)->render($generator); diff --git a/src/Attribute/Aggregate/AsArrayConstraint.php b/src/Attribute/Aggregate/AsArrayConstraint.php index 43f376c..b04090a 100644 --- a/src/Attribute/Aggregate/AsArrayConstraint.php +++ b/src/Attribute/Aggregate/AsArrayConstraint.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface; use Bdf\Form\Constraint\Closure; +use Override; use ReflectionMethod; use Symfony\Component\Validator\Constraint; @@ -73,17 +74,13 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ + #[Override] public function targetElements(): array { return [$this->target]; } - /** - * {@inheritdoc} - */ + #[Override] public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface { return new CallbackArrayConstraint($method->getName(), $this->message); diff --git a/src/Attribute/Aggregate/CallbackArrayConstraint.php b/src/Attribute/Aggregate/CallbackArrayConstraint.php index fcae412..202a6d5 100644 --- a/src/Attribute/Aggregate/CallbackArrayConstraint.php +++ b/src/Attribute/Aggregate/CallbackArrayConstraint.php @@ -10,6 +10,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Constraint\Closure; use Nette\PhpGenerator\Literal; +use Override; use Symfony\Component\Validator\Constraint; /** @@ -74,9 +75,7 @@ public function __construct( private ?string $message = null, ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $constraint = new Closure($form->{$this->methodName}(...), $this->message); @@ -84,9 +83,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $builder->arrayConstraint($constraint); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->use(Closure::class, 'ClosureConstraint'); diff --git a/src/Attribute/Aggregate/Count.php b/src/Attribute/Aggregate/Count.php index 9dd9d10..6e30f6c 100644 --- a/src/Attribute/Aggregate/Count.php +++ b/src/Attribute/Aggregate/Count.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; use Symfony\Component\Validator\Constraints\Count as CountConstraint; /** @@ -41,25 +42,19 @@ #[Attribute(Attribute::TARGET_PROPERTY)] final class Count extends CountConstraint implements ChildBuilderAttributeInterface { - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->arrayConstraint($this); } - /** - * {@inheritdoc} - */ + #[Override] public function validatedBy(): string { return CountConstraint::class . 'Validator'; } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $defaultParameters = get_class_vars(CountConstraint::class); diff --git a/src/Attribute/Aggregate/ElementType.php b/src/Attribute/Aggregate/ElementType.php index 171ea0d..f77dd69 100644 --- a/src/Attribute/Aggregate/ElementType.php +++ b/src/Attribute/Aggregate/ElementType.php @@ -11,6 +11,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementInterface; use Nette\PhpGenerator\Literal; +use Override; /** * Attribute for define the array element type @@ -66,18 +67,14 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $configurator = $this->configurator !== null ? [$form, $this->configurator] : null; $builder->element($this->elementType, $configurator); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $elementType = new Literal($generator->useAndSimplifyType($this->elementType)); diff --git a/src/Attribute/AttributeForm.php b/src/Attribute/AttributeForm.php index c4ca0d8..15f0bbb 100644 --- a/src/Attribute/AttributeForm.php +++ b/src/Attribute/AttributeForm.php @@ -9,11 +9,12 @@ use Bdf\Form\Attribute\Processor\PostConfigureInterface; use Bdf\Form\Attribute\Processor\ReflectionProcessor; use Bdf\Form\Custom\CustomForm; +use Override; /** * Utility class for declare a form using PHP 8 attributes and declare elements using typed properties * - * @template T + * @template T as array|object * @extends CustomForm * * @api @@ -48,17 +49,13 @@ public function __construct(?FormBuilderInterface $builder = null, ?AttributesPr $this->processor = $processor ?? new ReflectionProcessor(new ConfigureFormBuilderStrategy()); } - /** - * {@inheritdoc} - */ + #[Override] protected function configure(FormBuilderInterface $builder): void { $this->postConfigure = $this->processor->configureBuilder($this, $builder); } - /** - * {@inheritdoc} - */ + #[Override] public function postConfigure(FormInterface $form): void { if ($this->postConfigure) { diff --git a/src/Attribute/Button/Groups.php b/src/Attribute/Button/Groups.php index 5df3540..5380811 100644 --- a/src/Attribute/Button/Groups.php +++ b/src/Attribute/Button/Groups.php @@ -9,6 +9,7 @@ use Bdf\Form\Button\ButtonBuilderInterface; use Bdf\Form\Button\ButtonInterface; use Bdf\Form\RootElementInterface; +use Override; /** * Attribute for define the validation group to use when the related button is clicked @@ -53,17 +54,13 @@ public function __construct(string ...$groups) $this->groups = $groups; } - /** - * {@inheritdoc} - */ + #[Override] public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface $builder): void { $builder->groups($this->groups); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line(' ->groups(?)', [$this->groups]); diff --git a/src/Attribute/Button/Value.php b/src/Attribute/Button/Value.php index 5d76312..c18985c 100644 --- a/src/Attribute/Button/Value.php +++ b/src/Attribute/Button/Value.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Button\ButtonBuilderInterface; +use Override; /** * Attribute for define the button value, used to check if the button is clicked @@ -32,7 +33,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Value implements ButtonBuilderAttributeInterface +final readonly class Value implements ButtonBuilderAttributeInterface { public function __construct( /** @@ -40,20 +41,15 @@ public function __construct( * @readonly */ private string $value, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnButtonBuilder(AttributeForm $form, ButtonBuilderInterface $builder): void { $builder->value($this->value); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForButtonBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line(' ->value(?)', [$this->value]); diff --git a/src/Attribute/Child/AsFilter.php b/src/Attribute/Child/AsFilter.php index cdc704b..51bfa44 100644 --- a/src/Attribute/Child/AsFilter.php +++ b/src/Attribute/Child/AsFilter.php @@ -5,6 +5,7 @@ use Attribute; use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface; +use Override; use ReflectionMethod; /** @@ -39,7 +40,7 @@ * @api */ #[Attribute(Attribute::TARGET_METHOD)] -final class AsFilter implements MethodChildBuilderAttributeInterface +final readonly class AsFilter implements MethodChildBuilderAttributeInterface { /** * @var list @@ -56,17 +57,13 @@ public function __construct(string ...$targets) $this->targets = $targets; } - /** - * {@inheritdoc} - */ + #[Override] public function targetElements(): array { return $this->targets; } - /** - * {@inheritdoc} - */ + #[Override] public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface { return new CallbackFilter($method->getName()); diff --git a/src/Attribute/Child/AsModelTransformer.php b/src/Attribute/Child/AsModelTransformer.php index 6a85f06..8448f09 100644 --- a/src/Attribute/Child/AsModelTransformer.php +++ b/src/Attribute/Child/AsModelTransformer.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\Element\CallbackTransformer; use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface; use Bdf\Form\Child\ChildBuilderInterface; +use Override; use ReflectionMethod; /** @@ -47,7 +48,7 @@ * @api */ #[Attribute(Attribute::TARGET_METHOD)] -final class AsModelTransformer implements MethodChildBuilderAttributeInterface +final readonly class AsModelTransformer implements MethodChildBuilderAttributeInterface { /** * @var list @@ -64,17 +65,13 @@ public function __construct(string ...$targets) $this->targets = $targets; } - /** - * {@inheritdoc} - */ + #[Override] public function targetElements(): array { return $this->targets; } - /** - * {@inheritdoc} - */ + #[Override] public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface { return new CallbackModelTransformer($method->getName()); diff --git a/src/Attribute/Child/CallbackFilter.php b/src/Attribute/Child/CallbackFilter.php index 3c0aace..ebedaf9 100644 --- a/src/Attribute/Child/CallbackFilter.php +++ b/src/Attribute/Child/CallbackFilter.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\Element\CallbackTransformer; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Add a filter on the child element, by using method @@ -41,7 +42,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CallbackFilter implements ChildBuilderAttributeInterface +final readonly class CallbackFilter implements ChildBuilderAttributeInterface { public function __construct( /** @@ -53,20 +54,15 @@ public function __construct( * @readonly */ private string $method, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->filter([$form, $this->method]); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->filter([$form, ?]);', [$name, $this->method]); diff --git a/src/Attribute/Child/CallbackModelTransformer.php b/src/Attribute/Child/CallbackModelTransformer.php index 701d080..ecaba17 100644 --- a/src/Attribute/Child/CallbackModelTransformer.php +++ b/src/Attribute/Child/CallbackModelTransformer.php @@ -16,6 +16,7 @@ use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\Method; use Nette\PhpGenerator\PsrPrinter; +use Override; /** * Add a model transformer on the child element, by using method @@ -75,7 +76,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class CallbackModelTransformer implements ChildBuilderAttributeInterface +final readonly class CallbackModelTransformer implements ChildBuilderAttributeInterface { public function __construct( /** @@ -100,12 +101,9 @@ public function __construct( * @readonly */ private ?string $toInput = null, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { if ($this->callback !== null) { @@ -121,10 +119,8 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ - public function transformToHttp($value, ElementInterface $input) + #[Override] + public function transformToHttp(mixed $value, ElementInterface $input): mixed { if ($this->toInput === null) { return $value; @@ -133,10 +129,8 @@ public function transformToHttp($value, ElementInterface $input) return $this->form->{$this->toInput}($value, $input); } - /** - * {@inheritdoc} - */ - public function transformFromHttp($value, ElementInterface $input) + #[Override] + public function transformFromHttp(mixed $value, ElementInterface $input): mixed { if ($this->toEntity === null) { return $value; @@ -147,9 +141,7 @@ public function transformFromHttp($value, ElementInterface $input) }); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { if ($this->callback !== null) { diff --git a/src/Attribute/Child/Configure.php b/src/Attribute/Child/Configure.php index c5f6a82..26baaa1 100644 --- a/src/Attribute/Child/Configure.php +++ b/src/Attribute/Child/Configure.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface; use Bdf\Form\Child\ChildBuilderInterface; +use Override; use ReflectionMethod; /** @@ -60,37 +61,28 @@ public function __construct( * @var non-empty-string * @readonly */ - private string $target - ) { - } + private readonly string $target, + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $form->{$this->target}($builder); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$form->?($?);', [$this->target, $name]); } - /** - * {@inheritdoc} - */ + #[Override] public function targetElements(): array { return [$this->target]; } - /** - * {@inheritdoc} - */ + #[Override] public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface { return new self($method->getName()); diff --git a/src/Attribute/Child/DefaultValue.php b/src/Attribute/Child/DefaultValue.php index 2c82c5f..bd5dcee 100644 --- a/src/Attribute/Child/DefaultValue.php +++ b/src/Attribute/Child/DefaultValue.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Define the input default value @@ -36,7 +37,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class DefaultValue implements ChildBuilderAttributeInterface +final readonly class DefaultValue implements ChildBuilderAttributeInterface { public function __construct( /** @@ -46,17 +47,13 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->default($this->default); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->default(?);', [$name, $this->default]); diff --git a/src/Attribute/Child/Dependencies.php b/src/Attribute/Child/Dependencies.php index adbc5b6..a8d9bda 100644 --- a/src/Attribute/Child/Dependencies.php +++ b/src/Attribute/Child/Dependencies.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Add dependencies on other sibling elements @@ -35,7 +36,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Dependencies implements ChildBuilderAttributeInterface +final readonly class Dependencies implements ChildBuilderAttributeInterface { /** * @var list @@ -52,17 +53,13 @@ public function __construct(string ...$dependencies) $this->dependencies = $dependencies; } - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->depends(...$this->dependencies); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->depends(...?);', [$name, $this->dependencies]); diff --git a/src/Attribute/Child/GetSet.php b/src/Attribute/Child/GetSet.php index 58c38bc..1380a93 100644 --- a/src/Attribute/Child/GetSet.php +++ b/src/Attribute/Child/GetSet.php @@ -10,6 +10,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\PropertyAccess\Getter; use Bdf\Form\PropertyAccess\Setter; +use Override; /** * Define simple hydrator and extractor on the element, using the name property or accessor name @@ -42,7 +43,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class GetSet implements ChildBuilderAttributeInterface +final readonly class GetSet implements ChildBuilderAttributeInterface { public function __construct( /** @@ -55,20 +56,15 @@ public function __construct( * @readonly */ private ?string $propertyName = null, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->hydrator(new Setter($this->propertyName))->extractor(new Getter($this->propertyName)); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->use(Setter::class)->use(Getter::class); diff --git a/src/Attribute/Child/HttpField.php b/src/Attribute/Child/HttpField.php index 0d62df6..4c28ed9 100644 --- a/src/Attribute/Child/HttpField.php +++ b/src/Attribute/Child/HttpField.php @@ -11,6 +11,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Child\Http\ArrayOffsetHttpFields; use LogicException; +use Override; /** * Define the HTTP field name for the child element @@ -42,7 +43,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class HttpField implements ChildBuilderAttributeInterface +final readonly class HttpField implements ChildBuilderAttributeInterface { public function __construct( /** @@ -52,12 +53,9 @@ public function __construct( * @readonly */ private string $name, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { if (!$builder instanceof ChildBuilder) { @@ -67,9 +65,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $builder->httpFields(new ArrayOffsetHttpFields($this->name)); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->use(ArrayOffsetHttpFields::class); diff --git a/src/Attribute/Child/ModelTransformer.php b/src/Attribute/Child/ModelTransformer.php index ba5ad1c..2fcfe4c 100644 --- a/src/Attribute/Child/ModelTransformer.php +++ b/src/Attribute/Child/ModelTransformer.php @@ -10,6 +10,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; use Nette\PhpGenerator\Literal; +use Override; /** * Add a model transformer on the child, using a transformer class @@ -36,7 +37,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class ModelTransformer implements ChildBuilderAttributeInterface +final readonly class ModelTransformer implements ChildBuilderAttributeInterface { public function __construct( /** @@ -56,17 +57,13 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->modelTransformer(new $this->transformerClass(...$this->constructorArguments)); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $transformer = $generator->useAndSimplifyType($this->transformerClass); diff --git a/src/Attribute/Constraint/AsConstraint.php b/src/Attribute/Constraint/AsConstraint.php index b2a02ff..f3d9bf0 100644 --- a/src/Attribute/Constraint/AsConstraint.php +++ b/src/Attribute/Constraint/AsConstraint.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface; use Bdf\Form\Constraint\Closure; use Bdf\Form\ElementBuilderInterface; +use Override; use ReflectionMethod; use Symfony\Component\Validator\Constraint; @@ -72,17 +73,14 @@ public function __construct( private ?string $message = null, ) {} - /** - * {@inheritdoc} - */ + #[Override] public function targetElements(): array { + /** @var list */ return [$this->target]; } - /** - * {@inheritdoc} - */ + #[Override] public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface { return new CallbackConstraint($method->getName(), $this->message); diff --git a/src/Attribute/Constraint/CallbackConstraint.php b/src/Attribute/Constraint/CallbackConstraint.php index 2f49f4b..7b99553 100644 --- a/src/Attribute/Constraint/CallbackConstraint.php +++ b/src/Attribute/Constraint/CallbackConstraint.php @@ -11,6 +11,7 @@ use Bdf\Form\Constraint\Closure; use Bdf\Form\ElementBuilderInterface; use Nette\PhpGenerator\Literal; +use Override; use Symfony\Component\Validator\Constraint; /** @@ -77,9 +78,7 @@ public function __construct( private ?string $message = null, ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $constraint = new Closure($form->{$this->methodName}(...), $this->message); @@ -87,9 +86,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ $builder->satisfy($constraint); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->use(Closure::class, 'ClosureConstraint'); diff --git a/src/Attribute/Constraint/Satisfy.php b/src/Attribute/Constraint/Satisfy.php index b4599c9..81f6892 100644 --- a/src/Attribute/Constraint/Satisfy.php +++ b/src/Attribute/Constraint/Satisfy.php @@ -9,6 +9,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation; use Bdf\Form\Child\ChildBuilderInterface; use Nette\PhpGenerator\Literal; +use Override; use Symfony\Component\Validator\Constraint; use function is_string; @@ -58,17 +59,13 @@ public function __construct( private Constraint $constraint, ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->satisfy($this->constraint); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $constraint = ObjectInstantiation::promotedProperties($this->constraint)->render($generator); diff --git a/src/Attribute/Element/AsTransformer.php b/src/Attribute/Element/AsTransformer.php index f26c0b1..0b78d39 100644 --- a/src/Attribute/Element/AsTransformer.php +++ b/src/Attribute/Element/AsTransformer.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\MethodChildBuilderAttributeInterface; use Bdf\Form\ElementBuilderInterface; +use Override; use ReflectionMethod; /** @@ -64,17 +65,13 @@ public function __construct(string ...$targets) $this->targets = $targets; } - /** - * {@inheritdoc} - */ + #[Override] public function targetElements(): array { return $this->targets; } - /** - * {@inheritdoc} - */ + #[Override] public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface { return new CallbackTransformer($method->getName()); diff --git a/src/Attribute/Element/CallbackTransformer.php b/src/Attribute/Element/CallbackTransformer.php index 0824fed..0814916 100644 --- a/src/Attribute/Element/CallbackTransformer.php +++ b/src/Attribute/Element/CallbackTransformer.php @@ -19,6 +19,7 @@ use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\Method; use Nette\PhpGenerator\PsrPrinter; +use Override; /** * Add a HTTP transformer on the child element, by using method @@ -104,9 +105,7 @@ public function __construct( private ?string $toHttp = null, ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { if ($this->callback !== null) { @@ -122,9 +121,7 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ + #[Override] public function transformToHttp(mixed $value, ElementInterface $input): mixed { if ($this->toHttp === null) { @@ -134,9 +131,7 @@ public function transformToHttp(mixed $value, ElementInterface $input): mixed return $this->form->{$this->toHttp}($value, $input); } - /** - * {@inheritdoc} - */ + #[Override] public function transformFromHttp(mixed $value, ElementInterface $input): mixed { if ($this->fromHttp === null) { @@ -148,9 +143,7 @@ public function transformFromHttp(mixed $value, ElementInterface $input): mixed }); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { if ($this->callback !== null) { diff --git a/src/Attribute/Element/Choices.php b/src/Attribute/Element/Choices.php index 3ea63d8..333925a 100644 --- a/src/Attribute/Element/Choices.php +++ b/src/Attribute/Element/Choices.php @@ -17,6 +17,7 @@ use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Leaf\StringElementBuilder; use Nette\PhpGenerator\Literal; +use Override; /** * Define available values choice for the element @@ -103,9 +104,7 @@ public function __construct( private array $options = [], ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $options = $this->options; @@ -122,9 +121,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ ); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $options = $this->options; diff --git a/src/Attribute/Element/Date/AfterField.php b/src/Attribute/Element/Date/AfterField.php index b231d6a..8c8e5b5 100644 --- a/src/Attribute/Element/Date/AfterField.php +++ b/src/Attribute/Element/Date/AfterField.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Define that the element date must be after the date of the other field @@ -37,7 +38,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class AfterField implements ChildBuilderAttributeInterface +final readonly class AfterField implements ChildBuilderAttributeInterface { public function __construct( /** @@ -60,20 +61,15 @@ public function __construct( * @var bool */ private bool $orEqual = false, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->afterField($this->field, $this->message, $this->orEqual); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): 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 504e059..74f4911 100644 --- a/src/Attribute/Element/Date/BeforeField.php +++ b/src/Attribute/Element/Date/BeforeField.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Define that the element date must be before the date of the other field @@ -37,7 +38,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class BeforeField implements ChildBuilderAttributeInterface +final readonly class BeforeField implements ChildBuilderAttributeInterface { public function __construct( /** @@ -60,20 +61,15 @@ public function __construct( * @var bool */ private bool $orEqual = false, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->beforeField($this->field, $this->message, $this->orEqual); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): 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 dcb2bf5..fb7713d 100644 --- a/src/Attribute/Element/Date/DateFormat.php +++ b/src/Attribute/Element/Date/DateFormat.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Define the date format to use for parse the date string @@ -34,7 +35,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class DateFormat implements ChildBuilderAttributeInterface +final readonly class DateFormat implements ChildBuilderAttributeInterface { public function __construct( /** @@ -46,20 +47,15 @@ public function __construct( * @see https://www.php.net/manual/en/datetime.createfromformat.php#refsect1-datetime.createfromformat-parameters For the format */ private string $format, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->format($this->format); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->format(?);', [$name, $this->format]); diff --git a/src/Attribute/Element/Date/DateTimeClass.php b/src/Attribute/Element/Date/DateTimeClass.php index 7154d9f..774884c 100644 --- a/src/Attribute/Element/Date/DateTimeClass.php +++ b/src/Attribute/Element/Date/DateTimeClass.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; use Nette\PhpGenerator\Literal; +use Override; /** * Define the DateTime type to use on the date input @@ -35,7 +36,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class DateTimeClass implements ChildBuilderAttributeInterface +final readonly class DateTimeClass implements ChildBuilderAttributeInterface { public function __construct( /** @@ -45,20 +46,15 @@ public function __construct( * @readonly */ private string $className, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->className($this->className); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): 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 d6187ee..0d781eb 100644 --- a/src/Attribute/Element/Date/ImmutableDateTime.php +++ b/src/Attribute/Element/Date/ImmutableDateTime.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Use a DateTimeImmutable type as date time on the element @@ -35,19 +36,15 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class ImmutableDateTime implements ChildBuilderAttributeInterface +final readonly class ImmutableDateTime implements ChildBuilderAttributeInterface { - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->immutable(); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->immutable();', [$name]); diff --git a/src/Attribute/Element/Date/Timezone.php b/src/Attribute/Element/Date/Timezone.php index 3ec7e9a..58ac15f 100644 --- a/src/Attribute/Element/Date/Timezone.php +++ b/src/Attribute/Element/Date/Timezone.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Define the time used for parse and convert the date @@ -34,7 +35,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Timezone implements ChildBuilderAttributeInterface +final readonly class Timezone implements ChildBuilderAttributeInterface { public function __construct( /** @@ -44,20 +45,15 @@ public function __construct( * @readonly */ private string $timezone, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->timezone($this->timezone); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->timezone(?);', [$name, $this->timezone]); diff --git a/src/Attribute/Element/IgnoreTransformerException.php b/src/Attribute/Element/IgnoreTransformerException.php index 0aed003..1e825ee 100644 --- a/src/Attribute/Element/IgnoreTransformerException.php +++ b/src/Attribute/Element/IgnoreTransformerException.php @@ -10,6 +10,7 @@ use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Util\ValidatorBuilderTrait; +use Override; /** * Ignore errors generated by transformers @@ -40,7 +41,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class IgnoreTransformerException implements ChildBuilderAttributeInterface +final readonly class IgnoreTransformerException implements ChildBuilderAttributeInterface { public function __construct( /** @@ -49,20 +50,15 @@ public function __construct( * @readonly */ private bool $ignore = true, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->ignoreTransformerException($this->ignore); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->ignoreTransformerException(?);', [$name, $this->ignore]); diff --git a/src/Attribute/Element/Raw.php b/src/Attribute/Element/Raw.php index 7de263a..3683bb9 100644 --- a/src/Attribute/Element/Raw.php +++ b/src/Attribute/Element/Raw.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Leaf\NumberElementBuilder; +use Override; /** * Change the raw number mode @@ -35,7 +36,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Raw implements ChildBuilderAttributeInterface +final readonly class Raw implements ChildBuilderAttributeInterface { public function __construct( /** @@ -44,20 +45,15 @@ public function __construct( * @readonly */ private bool $flag = true, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->raw($this->flag); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->raw(?);', [$name, $this->flag]); diff --git a/src/Attribute/Element/Required.php b/src/Attribute/Element/Required.php index 4f2629a..3d4668c 100644 --- a/src/Attribute/Element/Required.php +++ b/src/Attribute/Element/Required.php @@ -8,6 +8,7 @@ use Bdf\Form\Attribute\ChildBuilderAttributeInterface; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Child\ChildBuilderInterface; +use Override; /** * Define the element as required @@ -35,7 +36,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY)] -final class Required implements ChildBuilderAttributeInterface +final readonly class Required implements ChildBuilderAttributeInterface { public function __construct( /** @@ -45,20 +46,15 @@ public function __construct( * @readonly */ private ?string $message = null, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $builder->required($this->message); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?->required(?);', [$name, $this->message]); diff --git a/src/Attribute/Element/Transformer.php b/src/Attribute/Element/Transformer.php index bf90a96..01767f1 100644 --- a/src/Attribute/Element/Transformer.php +++ b/src/Attribute/Element/Transformer.php @@ -13,6 +13,7 @@ use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; use Nette\PhpGenerator\Literal; +use Override; /** * Add a transformer on the element, using a transformer class @@ -51,14 +52,14 @@ public function __construct( * @var class-string * @readonly */ - private string $transformerClass, + private readonly string $transformerClass, /** * Arguments to provide on the transformer constructor * * @var array * @readonly */ - private array $constructorArguments = [], + private readonly array $constructorArguments = [], /** * Apply the transformer on the whole array element * instead of each element @@ -72,13 +73,10 @@ public function __construct( * * @see ArrayTransformer Prefer use this attribute for array element, instead of manually set this flag */ - private bool $array = false, - ) { - } + private readonly bool $array = false, + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { $transformer = new $this->transformerClass(...$this->constructorArguments); @@ -91,9 +89,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ } } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $transformer = $generator->useAndSimplifyType($this->transformerClass); diff --git a/src/Attribute/Element/TransformerError.php b/src/Attribute/Element/TransformerError.php index 1c646ab..3051690 100644 --- a/src/Attribute/Element/TransformerError.php +++ b/src/Attribute/Element/TransformerError.php @@ -10,6 +10,7 @@ use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; +use Override; /** * Fine grain configure error triggered by transformers @@ -41,7 +42,7 @@ * @api */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -class TransformerError implements ChildBuilderAttributeInterface +final readonly class TransformerError implements ChildBuilderAttributeInterface { public function __construct( /** @@ -69,12 +70,9 @@ public function __construct( * @readonly */ private ?string $validationCallback = null, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void { if ($this->message !== null) { @@ -90,9 +88,7 @@ public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $ } } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$?', [$name]); diff --git a/src/Attribute/Form/CallbackGenerator.php b/src/Attribute/Form/CallbackGenerator.php index eaaff19..386b2b9 100644 --- a/src/Attribute/Form/CallbackGenerator.php +++ b/src/Attribute/Form/CallbackGenerator.php @@ -9,6 +9,7 @@ use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Nette\PhpGenerator\Method; +use Override; /** * Define the value generator of the form, using a callback method @@ -39,7 +40,7 @@ * @api */ #[Attribute(Attribute::TARGET_CLASS)] -final class CallbackGenerator implements FormBuilderAttributeInterface +final readonly class CallbackGenerator implements FormBuilderAttributeInterface { public function __construct( /** @@ -50,20 +51,15 @@ public function __construct( * @readonly */ private string $callback, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void { $builder->generates([$form, $this->callback]); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line('$builder->generates([$this, ?]);', [$this->callback]); diff --git a/src/Attribute/Form/Csrf.php b/src/Attribute/Form/Csrf.php index 13c8808..5e5c25c 100644 --- a/src/Attribute/Form/Csrf.php +++ b/src/Attribute/Form/Csrf.php @@ -7,6 +7,7 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator; use Bdf\Form\Csrf\CsrfElementBuilder; +use Override; /** * Add a CSRF check on the form @@ -35,7 +36,7 @@ * @api */ #[Attribute(Attribute::TARGET_CLASS)] -final class Csrf implements FormBuilderAttributeInterface +final readonly class Csrf implements FormBuilderAttributeInterface { public function __construct( /** @@ -74,12 +75,9 @@ public function __construct( * @see CsrfElementBuilder::invalidate() The called method if defined */ private ?bool $invalidate = null, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void { $csrf = $builder->csrf($this->name); @@ -97,9 +95,7 @@ public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $bu } } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void { $parameters = [$this->name]; diff --git a/src/Attribute/Form/Generates.php b/src/Attribute/Form/Generates.php index 900d259..3c6f131 100644 --- a/src/Attribute/Form/Generates.php +++ b/src/Attribute/Form/Generates.php @@ -9,6 +9,7 @@ use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy; use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\Method; +use Override; /** * Define the value generator of the form, using a class name @@ -36,7 +37,7 @@ * @api */ #[Attribute(Attribute::TARGET_CLASS)] -final class Generates implements FormBuilderAttributeInterface +final readonly class Generates implements FormBuilderAttributeInterface { public function __construct( /** @@ -46,20 +47,15 @@ public function __construct( * @readonly */ private string $className, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function applyOnFormBuilder(AttributeForm $form, FormBuilderInterface $builder): void { $builder->generates($this->className); } - /** - * {@inheritdoc} - */ + #[Override] public function generateCodeForFormBuilder(AttributesProcessorGenerator $generator, AttributeForm $form): void { $generator->line( diff --git a/src/Attribute/Processor/CompileAttributesProcessor.php b/src/Attribute/Processor/CompileAttributesProcessor.php index 84d81ef..addb6f3 100644 --- a/src/Attribute/Processor/CompileAttributesProcessor.php +++ b/src/Attribute/Processor/CompileAttributesProcessor.php @@ -6,6 +6,7 @@ use Bdf\Form\Aggregate\FormBuilderInterface; use Bdf\Form\Attribute\AttributeForm; use LogicException; +use Override; /** * Processor for compile attributes to native PHP code for build the form @@ -19,7 +20,7 @@ * * @api */ -final class CompileAttributesProcessor implements AttributesProcessorInterface +final readonly class CompileAttributesProcessor implements AttributesProcessorInterface { public function __construct( /** @@ -30,21 +31,21 @@ public function __construct( * * @var callable(AttributeForm):non-empty-string */ - private $classNameResolver, + private mixed $classNameResolver, /** * Resolve the file name from the generated processor class name * * @var callable(class-string):non-empty-string */ - private $fileNameResolver, - ) { - } + private mixed $fileNameResolver, + ) {} /** * {@inheritdoc} * * @psalm-suppress PossiblyUnusedReturnValue */ + #[Override] public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): PostConfigureInterface { /** @var class-string $className */ diff --git a/src/Attribute/Processor/ConfigureFormBuilderStrategy.php b/src/Attribute/Processor/ConfigureFormBuilderStrategy.php index 721ad2d..7a40b8b 100644 --- a/src/Attribute/Processor/ConfigureFormBuilderStrategy.php +++ b/src/Attribute/Processor/ConfigureFormBuilderStrategy.php @@ -13,6 +13,7 @@ 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; @@ -36,9 +37,7 @@ public function __construct() $this->registerElementAttributeProcessor(new ExtractorAttributeProcessor()); } - /** - * {@inheritdoc} - */ + #[Override] public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { foreach ($formClass->getAttributes(FormBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { @@ -46,9 +45,7 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For } } - /** - * {@inheritdoc} - */ + #[Override] public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $submitBuilder = $builder->submit($name); @@ -58,9 +55,7 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att } } - /** - * {@inheritdoc} - */ + #[Override] public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $elementBuilder = $builder->add($name, $elementType); @@ -85,9 +80,7 @@ public function onElementProperty(ReflectionProperty $property, string $name, st } } - /** - * {@inheritdoc} - */ + #[Override] public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface { return new PostConfigureReflectionSetProperties($metadata->elementProperties(), $metadata->buttonProperties()); diff --git a/src/Attribute/Processor/Element/ConstraintAttributeProcessor.php b/src/Attribute/Processor/Element/ConstraintAttributeProcessor.php index 302a690..a91e385 100644 --- a/src/Attribute/Processor/Element/ConstraintAttributeProcessor.php +++ b/src/Attribute/Processor/Element/ConstraintAttributeProcessor.php @@ -4,6 +4,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; +use Override; use Symfony\Component\Validator\Constraint; /** @@ -18,25 +19,19 @@ final class ConstraintAttributeProcessor implements ElementAttributeProcessorInt { use SimpleMethodCallGeneratorTrait; - /** - * {@inheritdoc} - */ + #[Override] public function type(): string { return Constraint::class; } - /** - * {@inheritdoc} - */ + #[Override] public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->satisfy($attribute); } - /** - * {@inheritdoc} - */ + #[Override] private function methodName(): string { return 'satisfy'; diff --git a/src/Attribute/Processor/Element/ExtractorAttributeProcessor.php b/src/Attribute/Processor/Element/ExtractorAttributeProcessor.php index 6e9ed3d..900a296 100644 --- a/src/Attribute/Processor/Element/ExtractorAttributeProcessor.php +++ b/src/Attribute/Processor/Element/ExtractorAttributeProcessor.php @@ -4,6 +4,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\PropertyAccess\ExtractorInterface; +use Override; /** * Define the extractor by calling extract() @@ -17,25 +18,19 @@ final class ExtractorAttributeProcessor implements ElementAttributeProcessorInte { use SimpleMethodCallGeneratorTrait; - /** - * {@inheritdoc} - */ + #[Override] public function type(): string { return ExtractorInterface::class; } - /** - * {@inheritdoc} - */ + #[Override] public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->extractor($attribute); } - /** - * {@inheritdoc} - */ + #[Override] private function methodName(): string { return 'extractor'; diff --git a/src/Attribute/Processor/Element/FilterAttributeProcessor.php b/src/Attribute/Processor/Element/FilterAttributeProcessor.php index d4bad1f..46a997f 100644 --- a/src/Attribute/Processor/Element/FilterAttributeProcessor.php +++ b/src/Attribute/Processor/Element/FilterAttributeProcessor.php @@ -4,6 +4,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\Filter\FilterInterface; +use Override; /** * Add the filter by calling filter() @@ -17,25 +18,19 @@ final class FilterAttributeProcessor implements ElementAttributeProcessorInterfa { use SimpleMethodCallGeneratorTrait; - /** - * {@inheritdoc} - */ + #[Override] public function type(): string { return FilterInterface::class; } - /** - * {@inheritdoc} - */ + #[Override] public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->filter($attribute); } - /** - * {@inheritdoc} - */ + #[Override] private function methodName(): string { return 'filter'; diff --git a/src/Attribute/Processor/Element/HydratorAttributeProcessor.php b/src/Attribute/Processor/Element/HydratorAttributeProcessor.php index 722e0a7..7f83fda 100644 --- a/src/Attribute/Processor/Element/HydratorAttributeProcessor.php +++ b/src/Attribute/Processor/Element/HydratorAttributeProcessor.php @@ -4,6 +4,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\PropertyAccess\HydratorInterface; +use Override; /** * Define as hydrator by calling hydrator() @@ -17,25 +18,19 @@ final class HydratorAttributeProcessor implements ElementAttributeProcessorInter { use SimpleMethodCallGeneratorTrait; - /** - * {@inheritdoc} - */ + #[Override] public function type(): string { return HydratorInterface::class; } - /** - * {@inheritdoc} - */ + #[Override] public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->hydrator($attribute); } - /** - * {@inheritdoc} - */ + #[Override] private function methodName(): string { return 'hydrator'; diff --git a/src/Attribute/Processor/Element/TransformerAttributeProcessor.php b/src/Attribute/Processor/Element/TransformerAttributeProcessor.php index dc3d386..4e74542 100644 --- a/src/Attribute/Processor/Element/TransformerAttributeProcessor.php +++ b/src/Attribute/Processor/Element/TransformerAttributeProcessor.php @@ -5,6 +5,7 @@ use Bdf\Form\Child\ChildBuilderInterface; use Bdf\Form\ElementBuilderInterface; use Bdf\Form\Transformer\TransformerInterface; +use Override; /** * Add the transformer by calling transformer() @@ -18,25 +19,19 @@ final class TransformerAttributeProcessor implements ElementAttributeProcessorIn { use SimpleMethodCallGeneratorTrait; - /** - * {@inheritdoc} - */ + #[Override] public function type(): string { return TransformerInterface::class; } - /** - * {@inheritdoc} - */ + #[Override] public function process(ChildBuilderInterface $builder, object $attribute): void { $builder->transformer($attribute); } - /** - * {@inheritdoc} - */ + #[Override] private function methodName(): string { return 'transformer'; diff --git a/src/Attribute/Processor/GenerateConfiguratorStrategy.php b/src/Attribute/Processor/GenerateConfiguratorStrategy.php index 503a884..283cdf8 100644 --- a/src/Attribute/Processor/GenerateConfiguratorStrategy.php +++ b/src/Attribute/Processor/GenerateConfiguratorStrategy.php @@ -16,6 +16,7 @@ use Bdf\Form\Attribute\Processor\Element\TransformerAttributeProcessor; use Nette\PhpGenerator\Closure; use Nette\PhpGenerator\Literal; +use Override; use ReflectionAttribute; use ReflectionClass; use ReflectionProperty; @@ -47,9 +48,7 @@ public function __construct(string $className) $this->registerElementAttributeProcessor(new ExtractorAttributeProcessor()); } - /** - * {@inheritdoc} - */ + #[Override] public function onFormClass(ReflectionClass $formClass, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $empty = true; @@ -64,9 +63,7 @@ public function onFormClass(ReflectionClass $formClass, AttributeForm $form, For } } - /** - * {@inheritdoc} - */ + #[Override] public function onButtonProperty(ReflectionProperty $property, string $name, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $this->generator->line('$builder->submit(?)', [$name]); @@ -78,9 +75,7 @@ public function onButtonProperty(ReflectionProperty $property, string $name, Att $this->generator->line(";\n"); } - /** - * {@inheritdoc} - */ + #[Override] public function onElementProperty(ReflectionProperty $property, string $name, string $elementType, AttributeForm $form, FormBuilderInterface $builder, ProcessorMetadata $metadata): void { $elementType = $this->generator->useAndSimplifyType($elementType); @@ -108,9 +103,7 @@ public function onElementProperty(ReflectionProperty $property, string $name, st $this->generator->line(); // Add empty line } - /** - * {@inheritdoc} - */ + #[Override] public function onPostConfigure(ProcessorMetadata $metadata, AttributeForm $form): ?PostConfigureInterface { $this->generator->line('return $this;'); diff --git a/src/Attribute/Processor/PostConfigureReflectionSetProperties.php b/src/Attribute/Processor/PostConfigureReflectionSetProperties.php index 87dfe26..876f03b 100644 --- a/src/Attribute/Processor/PostConfigureReflectionSetProperties.php +++ b/src/Attribute/Processor/PostConfigureReflectionSetProperties.php @@ -4,12 +4,13 @@ use Bdf\Form\Aggregate\FormInterface; use Bdf\Form\Attribute\AttributeForm; +use Override; use ReflectionProperty; /** * Fill the form properties using reflection */ -final class PostConfigureReflectionSetProperties implements PostConfigureInterface +final readonly class PostConfigureReflectionSetProperties implements PostConfigureInterface { public function __construct( /** @@ -26,12 +27,9 @@ public function __construct( * @var array */ private array $buttonProperties, - ) { - } + ) {} - /** - * {@inheritdoc} - */ + #[Override] public function postConfigure(AttributeForm $form, FormInterface $inner): void { foreach ($this->elementProperties as $name => $reflection) { diff --git a/src/Attribute/Processor/ReflectionProcessor.php b/src/Attribute/Processor/ReflectionProcessor.php index ec0c88f..b0f9311 100644 --- a/src/Attribute/Processor/ReflectionProcessor.php +++ b/src/Attribute/Processor/ReflectionProcessor.php @@ -6,6 +6,7 @@ use Bdf\Form\Attribute\AttributeForm; use Bdf\Form\Button\ButtonInterface; use Bdf\Form\ElementInterface; +use Override; use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; @@ -19,7 +20,7 @@ * * @api */ -final class ReflectionProcessor implements AttributesProcessorInterface +final readonly class ReflectionProcessor implements AttributesProcessorInterface { /** * Strategy to use on each field / class @@ -36,9 +37,7 @@ public function __construct(ReflectionStrategyInterface $strategy) $this->strategy = $strategy; } - /** - * {@inheritdoc} - */ + #[Override] public function configureBuilder(AttributeForm $form, FormBuilderInterface $builder): ?PostConfigureInterface { $metadata = new ProcessorMetadata(); @@ -102,7 +101,7 @@ private function iterateClassHierarchy(AttributeForm $form): iterable */ private function registerMethodsMetadata(AttributeForm $form, ProcessorMetadata $metadata): void { - foreach ((new ReflectionClass($form))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + foreach (new ReflectionClass($form)->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { foreach ($method->getAttributes(MethodChildBuilderAttributeInterface::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { /** @var MethodChildBuilderAttributeInterface $attributeInstance */ $attributeInstance = $attribute->newInstance(); From 57fb8a0061428dfe2d37b2919fb131ed0b8bbf66 Mon Sep 17 00:00:00 2001 From: Vincent QUATREVIEUX Date: Wed, 18 Mar 2026 17:54:50 +0100 Subject: [PATCH 40/40] ci: disable jit on psalm --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 46d82c5..a7ffcee 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -97,7 +97,7 @@ jobs: with: php-version: 8.4 extensions: json - ini-values: date.timezone=Europe/Paris + ini-values: date.timezone=Europe/Paris, opcache.jit=disable - name: Install Infection run: |