Declaring forms using PHP 8 attributes and typed properties, over BDF form
composer require b2pweb/bdf-form-attribute
To create a form using PHP 8 attributes, first you have to extend AttributeForm.
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...
#[Positive, UnitTransformer, GetSet]
public IntegerElement $weight;Adaptation of example from BDF Form : Handle entities
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);
}
}This library supports various attributes types for configure form elements :
- Symfony validator's
Constraint, translated as...->satisfy(new Constraint(...)) ExtractorInterface, translated as...->extractor(new Extractor(...))HydratorInterface, translated as...->hydrator(new Hydrator(...))FilterInterface, translated as...->filter(new Filter(...))TransformerInterface, translated as...->transformer(new Transformer(...))
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 as argument of form constructor.
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());| Attribute | Example | Translated to | Purpose |
|---|---|---|---|
Generates |
Generates(MyEntity::class) |
$builder->generates(MyEntity::class) |
Define the entity class generated by the form. |
CallbackGenerator |
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 |
Csrf(tokenId: 'MyToken') |
$builder->csrf()->tokenId('MyToken') |
Add a CSRF element on the form. |
| Attribute | Example | Translated to | Purpose |
|---|---|---|---|
AsConstraint |
AsConstraint('validateFoo') |
$builder->satisfy([$this, 'validateFoo']) |
Use the method as constraint for the target element. |
AsArrayConstraint |
AsArrayConstraint('validateFoo') |
$builder->arrayConstraint([$this, 'validateFoo']) |
Use the method as constraint for the target array element. |
AsFilter |
AsFilter('filterFoo') |
$builder->filter([$this, 'filterFoo']) |
Use the method as filter for the target element. |
AsTransformer |
AsTransformer('transformFoo') |
$builder->transformer([$this, 'transformFoo']) |
Use the method as HTTP transformer for the target element. |
AsModelTransformer |
AsModelTransformer('transformFoo') |
$builder->modelTransformer([$this, 'transformFoo']) |
Use the method as model transformer for the target element. |
| Attribute | Example | Translated to | Purpose |
|---|---|---|---|
Groups |
Groups('foo', 'bar') |
...->groups(['foo', 'bar']) |
Define validation groups to use when the given button is clicked. |
Value |
Value('foo') |
...->value('foo') |
Define the button value. |


