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 |
---|---|---|---|
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. |
Attribute | Example | Translated to | Purpose |
---|---|---|---|
Child | |||
ModelTransformer |
ModelTransformer(MyTransformer::class, ['ctroarg']) |
...->modelTransformer(new MyTransformer('ctorarg)) |
Define a model transformer on the current child. |
CallbackModelTransformer |
CallbackModelTransformer(toEntity: 'parseInput', toInput: 'normalize') |
...->modelTransformer(fn ($value, $input, $toEntity) => $toEntity ? $this->parseInput($value, $input) : $this->normalize($value, $input)) |
Define a model transformer using a form method. |
Configure |
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 |
DefaultValue(42) |
...->configureField($elementBuilder) |
Define the default value of the input. |
Dependencies |
Dependencies('foo', 'bar') |
...->depends('foo', 'bar') |
Declare dependencies on the current input. Dependencies will be submitted before the current field. |
GetSet |
GetSet('realField') |
...->getter('realField')->setter('realField') |
Enable hydration and extraction of the entity. |
Element | |||
CallbackConstraint |
CallbackConstraint('validateInput') |
...->satisfy([$this, 'validateInput']) |
Validate an input using a method. |
Satisfy |
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 |
Transformer(MyTransformer::class, ['ctorarg']) |
...->transformer(new MyTransformer('ctorarg)) |
Add a transformer on the input. Prefer directly use the transformer class as attribute if possible. |
CallbackTransformer |
CallbackTransformer(fromHttp: 'parse', toHttp: 'stringify') |
...->transformer(fn ($value, $input, $toPhp) => $toPhp ? $this->parse($value, $input) : $this->stringify($value, $input)) |
Add a transformer using a form method. |
Choices |
Choices(['foo', 'bar']) |
...->choices(['foo', 'bar']) |
Define the values choices of the input. Supports using a method as choices provider. |
Raw |
Raw |
...->raw() |
For number elements. Use native PHP cast instead of locale parsing for convert number. |
TransformerError |
TransformerError(message: 'Invalid value provided') |
...->transformerErrorMessage('Invalid value provided') |
Configure error handling of transformer exceptions. |
IgnoreTransformerException |
IgnoreTransformerException |
...->ignoreTransformerException() |
Ignore transformer exception. If enable and an exception occurs, the raw value will be used. |
DateTimeElement | |||
DateFormat |
DateFormat('d/m/Y H:i') |
...->format('d/m/Y H:i') |
Define the input date format. |
DateTimeClass |
DateTimeClass(Carbon::class) |
...->className(Carbon::class) |
Define date time class to use on for parse the date. |
ImmutableDateTime |
ImmutableDateTime |
...->immutable() |
Use DateTimeImmutable as date time class. |
Timezone |
Timezone('Europe/Paris') |
...->timezone('Europe/Paris') |
Define the parsing and normalized timezone to use. |
ArrayElement | |||
ArrayConstraint |
ArrayConstraint(MyConstraint::class, ['opt' => 'val']) |
...->arrayConstraint(new MyConstraint(['opt' => 'val'])) |
Add a constraint on the whole array element. |
CallbackArrayConstraint |
CallbackArrayConstraint('validateInput') |
...->arrayConstraint([$this, 'validateInput']) |
Add a constraint on the whole array element, using a form method. |
Count |
Count(min: 3, max: 6) |
...->arrayConstraint(new Count(min: 3, max: 6)) |
Add a Count constraint on the array element. |
ElementType |
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. |