/bdf-form-attribute

Declaring forms using PHP 8 attributes and typed properties, over BDF form

Primary LanguagePHPMIT LicenseMIT

BDF Form attribute

build Scrutinizer Code Quality Code Coverage Packagist Version Total Downloads Type Coverage

Declaring forms using PHP 8 attributes and typed properties, over BDF form

Usage

Install using composer

composer require b2pweb/bdf-form-attribute

Declare a form class

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);
    }
}

Supported attributes

This library supports various attributes types for configure form elements :

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 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());

Available attributes

On form class

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.

On button property

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.

On element property

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.