Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
834a982
Initial commit
vincent4vx Dec 2, 2021
c932b25
Add description on composer.json
vincent4vx Dec 3, 2021
2675e98
Use PHP 8.0 for analysis
vincent4vx Dec 3, 2021
1c56b2c
Rename Annotation to Attribute
vincent4vx Dec 3, 2021
a2b4360
Improve type coverage
vincent4vx Dec 3, 2021
52f7748
Add infection and phpcs
vincent4vx Dec 3, 2021
3351e63
Refactor attribute processor system
vincent4vx Dec 3, 2021
90a951a
Fix check style
vincent4vx Dec 6, 2021
bf1aeff
Add code generation
vincent4vx Dec 8, 2021
42997ae
Add new attributes and solve TODOs
vincent4vx Dec 8, 2021
20d86b3
Add scrutinizer config + fix timezone on CI
vincent4vx Dec 8, 2021
050607a
Fix scrutinizer build: add XDEBUG_MODE=coverage on command
vincent4vx Dec 8, 2021
69eee08
Add README
vincent4vx Dec 8, 2021
07472e0
Improve readme
vincent4vx Dec 9, 2021
8f964d5
Fix symfony validator link on readme
vincent4vx Dec 9, 2021
47ac3e8
Set attributes properties visibility to private
vincent4vx Dec 9, 2021
af7c0e5
Fix builder calls order shoud corresponds to attributes order
vincent4vx Dec 9, 2021
f2938b4
Use LazyChoice instead of LazzyChoice
vincent4vx Dec 9, 2021
7fceed0
Update composer.json
vincent4vx Dec 9, 2021
b509238
Define branch alias to v1.0
vincent4vx Dec 10, 2021
61bb761
PHP 8.1 compatibility
vincent4vx Feb 1, 2022
cdc30db
chore: Compatibility with PHP 8.2
vincent4vx Jan 6, 2023
05c33a0
ci: fix composer for PHP 8.0
vincent4vx Jan 6, 2023
5e7951d
chore: Compatibility with PHP 8.3
vincent4vx Feb 28, 2024
2b2f8df
chore: Compatibility nette generator 4
vincent4vx Feb 28, 2024
4f3d9c9
chore: Update to psalm 5
vincent4vx Mar 4, 2024
a79cbb6
feat: Add new attributes + allow to annotation methods (#FRAM-173)
vincent4vx Jul 26, 2024
70845c1
ci: fix psalm annotations
vincent4vx Jul 26, 2024
0f92f38
doc: Update README with new attributes
vincent4vx Jul 26, 2024
7426b93
chore: Compatibility with PHP 8.4 (#FRAM-177)
vincent4vx Oct 11, 2024
8f8f3fa
ci: test with php 8.4
vincent4vx Dec 3, 2024
b29d6f0
ci: try to fix infection tests
vincent4vx Dec 3, 2024
79ea252
ci: update infection
vincent4vx Dec 3, 2024
b8e137e
chore: compatibility with PHP 8.5
vincent4vx Nov 21, 2025
74aa7a5
Merge pull request #1 from b2pweb/feature-FRAM-173-improve-api
vincent4vx Mar 3, 2026
ad3f86d
chore: Prepare merge with bdf-form
vincent4vx Mar 18, 2026
cf85d6d
chore: remove file that conflict with bdf-form repository
vincent4vx Mar 18, 2026
32e4363
Merge remote-tracking branch 'attribute/chore-prepare-merge' into cho…
vincent4vx Mar 18, 2026
d6dbef5
fix(attribute): Compatibility with form v2
vincent4vx Mar 18, 2026
30c2fed
fix(attribute): SF 6 constraint compatibility
vincent4vx Mar 18, 2026
5dc191e
ci(attribute): fix psalm errors
vincent4vx Mar 18, 2026
57fb8a0
ci: disable jit on psalm
vincent4vx Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.idea
/vendor
/composer.lock
.phpunit.result.cache
infection.log
178 changes: 178 additions & 0 deletions README.md

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"name": "b2pweb/bdf-form",
"description": "Simple and flexible form library",
"keywords": ["attributes", "PHP 8", "form", "validator"],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Vincent Quatrevieux"
"name": "Vincent Quatrevieux",
"email": "vquatrevieux@b2pweb.com"
}
],
"autoload": {
Expand All @@ -15,19 +17,21 @@
},
"autoload-dev": {
"psr-4": {
"Bdf\\Form\\": "tests"
"Bdf\\Form\\": "tests",
"Tests\\Form\\Attribute\\": "tests/Attribute"
}
},
"require": {
"php": "~8.4",
"symfony/property-access": "~6.4|~7.0|~8.0",
"symfony/validator": "~6.4|~7.0|~8.0"
"symfony/validator": "~6.4|~7.0|~8.0",
"nette/php-generator": "~3.6|~4.0"
},
"require-dev": {
"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"
},
Expand Down
76 changes: 76 additions & 0 deletions src/Attribute/Aggregate/ArrayConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Bdf\Form\Attribute\Aggregate;

use Attribute;
use Bdf\Form\Aggregate\ArrayElementBuilder;
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\CodeGenerator\ObjectInstantiation;
use Bdf\Form\Child\ChildBuilderInterface;
use Override;
use Symfony\Component\Validator\Constraint;

/**
* Add a constraint on the whole array element
* Use Satisfy, or directly the constraint as attribute for add a constraint on one array item
*
* This attribute is equivalent to call :
* <code>
* $builder->array('values')->arrayConstraints(MyConstraint::class, $options);
* </code>
*
* Usage:
* <code>
* class MyForm extends AttributeForm
* {
* #[ArrayConstraint(Unique::class, ['message' => 'My error'])]
* private ArrayElement $values;
*
* // or on PHP 8.1
* #[ArrayConstraint(new Unique(['message' => 'My error']))]
* private ArrayElement $values;
* }
* </code>
*
* @see Satisfy Attribute for add constraint for items
* @see ArrayElementBuilder::arrayConstraint() The called method
* @see CallbackArrayConstraint Use for a custom method validation
*
* @implements ChildBuilderAttributeInterface<ArrayElementBuilder>
* @api
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final readonly class ArrayConstraint 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.
* This may not work if the constraint has a complex constructor.
*
* @var Constraint
*/
private Constraint $constraint,
) {}

#[Override]
public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
{
$builder->arrayConstraint($this->constraint);
}

#[Override]
public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
{
$constraint = ObjectInstantiation::promotedProperties($this->constraint)->render($generator);
$generator->line('$?->arrayConstraint(?);', [$name, $constraint]);
}
}
45 changes: 45 additions & 0 deletions src/Attribute/Aggregate/ArrayTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Bdf\Form\Attribute\Aggregate;

use Attribute;
use Bdf\Form\Attribute\Element\Transformer;
use Bdf\Form\Transformer\TransformerInterface;

/**
* Add a transformer on the array element, using a transformer class
*
* This attribute will simply set the array flag to true on the {@see Transformer} attribute.
*
* This attribute is equivalent to call :
* <code>
* $builder->string('foo')->arrayTransformer(new MyTransformer(...$arguments));
* </code>
*
* Usage:
* <code>
* class MyForm extends AttributeForm
* {
* #[ArrayTransformer(MyTransformer::class, ['foo', 'bar']), ElementType(IntegerElement::class)]
* private ArrayElement $foo;
* }
* </code>
*
* @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<TransformerInterface> $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);
}
}
88 changes: 88 additions & 0 deletions src/Attribute/Aggregate/AsArrayConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Bdf\Form\Attribute\Aggregate;

use Attribute;
use Bdf\Form\Aggregate\ArrayElementBuilder;
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;

/**
* Define the annotated method as a custom constraint for an array element
*
* 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
*
* This attribute is equivalent to call :
* <code>
* $builder->array('foo')->arrayConstraint([$this, 'validateFoo'], 'Foo is invalid');
* </code>
*
* Usage:
* <code>
* 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;
* }
* }
* </code>
*
* @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<ArrayElementBuilder>
*
* @api
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final readonly class AsArrayConstraint implements MethodChildBuilderAttributeInterface
{
public function __construct(
/**
* The element property name to which the constraint is applied
*
* @var non-empty-string
* @readonly
*/
private string $target,
/**
* The error message to use
* This option is used only if the validator return false, in other cases,
* the message returned by the validator will be used
*
* @var string|null
* @readonly
*/
private ?string $message = null,
) {
}

#[Override]
public function targetElements(): array
{
return [$this->target];
}

#[Override]
public function attribute(ReflectionMethod $method): ChildBuilderAttributeInterface
{
return new CallbackArrayConstraint($method->getName(), $this->message);
}
}
98 changes: 98 additions & 0 deletions src/Attribute/Aggregate/CallbackArrayConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace Bdf\Form\Attribute\Aggregate;

use Attribute;
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 Bdf\Form\Constraint\Closure;
use Nette\PhpGenerator\Literal;
use Override;
use Symfony\Component\Validator\Constraint;

/**
* Define a custom constraint for an array element, using a validation method
*
* This attribute is equivalent to call :
* <code>
* $builder->array('foo')->arrayConstraint([$this, 'validateFoo'], 'Foo is invalid');
* </code>
*
* Usage:
* <code>
* 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;
* }
* }
* </code>
*
* @see ArrayElementBuilder::arrayConstraint() The called method
* @see Constraint
* @see Closure The used constraint
* @see ArrayConstraint Use for a class constraint
*
* @implements ChildBuilderAttributeInterface<ArrayElementBuilder>
*
* @api
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final readonly 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 non-empty-string
* @readonly
*/
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
*/
private ?string $message = null,
) {}

#[Override]
public function applyOnChildBuilder(AttributeForm $form, ChildBuilderInterface $builder): void
{
$constraint = new Closure($form->{$this->methodName}(...), $this->message);

$builder->arrayConstraint($constraint);
}

#[Override]
public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, AttributeForm $form): void
{
$generator->use(Closure::class, 'ClosureConstraint');

$parameters = $this->message !== null
? new Literal('$form->?(...), ?', [$this->methodName, $this->message])
: new Literal('$form->?(...)', [$this->methodName])
;

$generator->line('$?->arrayConstraint(new ClosureConstraint(?));', [$name, $parameters]);
}
}
Loading
Loading