Slide 1

Slide 1 text

Symfony Form Practical Use Cases Alexandre Salomé - December 2024

Slide 2

Slide 2 text

Remastered re-edition 2 🔴 Recording… Original 2013 @ Paris Re-edited 2021 @ Online Remastered Now and here

Slide 3

Slide 3 text

What about you? 3

Slide 4

Slide 4 text

What’s new? API is stable. Features are awesome! Symfony 7.2 features : - Lazy choice loader - Stateless CSRF Check Symfony Blog. Subscribe to living on the edge. 4

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

75% of Symfony UX demos are about forms: - Auto-validating form ❤ - Embedded CollectionType form - Dependent form fields - Up & Down Voting - Inline editing - Invoice creator - Product form - File upload 30% of the components are about form: - Autocompleter - Image Cropper - Stylized Dropzone - Toggle password 6 Symfony UX

Slide 7

Slide 7 text

New use cases In this new edition: 1. Creating a form 2. Required red asterisk 3. Dependent form fields 4. Form translations 7

Slide 8

Slide 8 text

Creating a Symfony Form 8

Slide 9

Slide 9 text

Bootstrapping a test application 9 # Create a new Symfony application symfony new --webapp symfony-form cd symfony-form # Add UX live component composer require symfony/ux-live-component # CSS php bin/console importmap:require bootstrap

Slide 10

Slide 10 text

class RegisterInformationType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('firstName'); $builder->add('lastName'); $builder->add('email'); $builder->addEventListener(FormEvents::PRE_SET_DATA, function ($event) { // ... }); $builder->add('submit', SubmitType::class, [ 'label' => 'Enregistrer', ]); } } 10 Creating custom types

Slide 11

Slide 11 text

class RegisterInformationType extends AbstractType { public function configureOptions(OptionsResolver $resolver): void { // Use existing options $resolver->setDefaults([ 'allow_extra_fields' => true, 'data_class' => User::class, 'validation_groups' => ['register'], ]); // Declare new options $resolver->setRequired('manager'); $resolver->setAllowedTypes('manager', [User::class]); } // … } 11 Form options

Slide 12

Slide 12 text

#[Route(path: '/register', name: 'app_register_information')] public function __invoke(): Response { return $this->render('pages/register_information.html.twig'); } 12 No more form handling in the controller

Slide 13

Slide 13 text

// src/Twig/Components/RegisterInformationForm.php #[AsLiveComponent] class RegisterInformationForm extends AbstractController { use ComponentWithFormTrait; use DefaultActionTrait; protected function instantiateForm(): FormInterface { return $this->createForm(RegisterInformationType::class, $data, [ 'manager' => $this->manager, ]); } } 13 Hello component - PHP - 1/2

Slide 14

Slide 14 text

// src/Twig/Components/RegisterInformationForm.php #[AsLiveComponent] class RegisterInformationForm extends AbstractController { // ... #[LiveAction] public function saveForm(): RedirectResponse { $this->submitForm(); $form = $this->getForm(); // Business logic here } } 14 Hello component - PHP - 2/2

Slide 15

Slide 15 text

{# templates/components/RegisterInformationForm.html.twig #}
{{ form(form, {attr: { Novalidate:'novalidate', 'data-action': 'live#action:prevent', 'data-live-action-param': 'saveForm', }}) }}
15 Hello component - Twig

Slide 16

Slide 16 text

16 Form types and options The form types are dynamic and extensible: class EntityType extends AbstractType { // ... public function getParent(): string { return ChoiceType::class; } // ... } The form options are convenient.

Slide 17

Slide 17 text

17 One type for all forms There is one type instance for all forms. The types build forms. They are not involved in the form lifecycle.

Slide 18

Slide 18 text

{# templates/form.html.twig #} {% use "bootstrap_5_layout.html.twig" %} {# Address by form name #} {% block _register_information_firstName_widget %} {# ... #} {% endblock %} {# Address by form type #} {% block register_information_firstName_widget %} {# ... #} {% endblock %} 18 Custom templating

Slide 19

Slide 19 text

Form naming 19 Instanciation method Input field names $formFactory->create( RegisterInformationType::class ); register_information[firstName] register_information[lastName] register_information[email] $formFactory->createNamed( 'foo', RegisterInformationType::class ); foo[firstName] foo[lastName] foo[email] $formFactory->createNamed( '', RegisterInformationType::class, ); firstName lastName email

Slide 20

Slide 20 text

20 Feature-complete forms - CSS frameworks templates - Security & validation - Live components Ajax validation ❤ Dynamic forms ❤

Slide 21

Slide 21 text

Required red asterisk 21

Slide 22

Slide 22 text

Required fields in forms 22 Required fields are marked with a red asterisk *

Slide 23

Slide 23 text

23 Extending framework templates rendering {# templates/form.html.twig #} {% use "bootstrap_5_layout.html.twig" %}

Slide 24

Slide 24 text

Form templates {# Recursive form rendering #} {% block ..._widget %} {% block ..._label %} {% block ..._errors %} {% block ..._row %} {# Parent type rendering #} {% block language_widget %} {% block choice_widget %} {% block form_widget %}

Slide 25

Slide 25 text

25 Form templates ROW WIDGET ERRORS LABEL

Slide 26

Slide 26 text

ROW WIDGET 26 Form templates ERRORS LABEL

Slide 27

Slide 27 text

ROW WIDGET 27 Form templates ERRORS LABEL ROW WIDGET ERRORS LABEL ROW WIDGET ERRORS LABEL ROW WIDGET ERRORS LABEL

Slide 28

Slide 28 text

28 Implementing the red asterisk {# templates/form.html.twig #} {% use "bootstrap_5_layout.html.twig" %} {% block form_label_content %} {{- parent() -}} {% if required %} * {% endif %} {% endblock %}

Slide 29

Slide 29 text

Required fields in forms 29 Required fields are marked with a red asterisk *

Slide 30

Slide 30 text

30 Dependent form fields

Slide 31

Slide 31 text

Eat something! 31 Similar to the Symfony UX demo.

Slide 32

Slide 32 text

First, you select a meal. Then, you select a dish. class EatForm extends AbstractType { private static array $choices = [ 'Breakfast' => [ 'Croissant', 'Pain au chocolat', ], 'Lunch' => [ 'Salad', 'Sandwich', 'Soup', ], 'Dinner' => [ 'Steak and potatoes', 'Fish and pasta', 'Chicken and rice', ], ]; } 32 Eat something!

Slide 33

Slide 33 text

// in src/Form/EatForm.php public function buildForm(FormBuilderInterface $builder, array $options): void { $meals = array_keys(self::$choices); $meals = array_combine($meals, $meals); // $meals = ["Breakfast" => "Breakfast", "Lunch" => "Lunch", ...]; $builder->add('meal', ChoiceType::class, [ 'choices' => $meals, 'placeholder' => 'Choose a meal', 'required' => true, ]); } 33 Add the meals list to our form

Slide 34

Slide 34 text

$builder = new DynamicFormBuilder($builder); $builder->addDependent( 'meal', 'dish', function (DependentField $field, ?string $meal) { if (!$meal) { return; } $field->add(ChoiceType::class, [ 'choices' => array_combine(self::$choices[$meal], self::$choices[$meal]), 'placeholder' => 'Choose a dish', 'required' => true, ]); } ); 34 Using Symfony Casts Builder

Slide 35

Slide 35 text

$listener = function (FormEvent $event) { $form = $event->getForm(); $data = $event->getData(); $meal = $data['meal'] ?? null; if ($meal && isset(self::$choices[$meal])) { $form->add('dish', ChoiceType::class, [ 'choices' => array_combine(self::$choices[$meal], self::$choices[$meal]), 'placeholder' => 'Choose a dish', 'required' => true, ]); } }; $builder->addEventListener(FormEvents::POST_SET_DATA, $listener); $builder->addEventListener(FormEvents::PRE_SUBMIT, $listener); 35 Reinventing the wheel

Slide 36

Slide 36 text

36

Slide 37

Slide 37 text

1. When a dish has already been chosen, you get an invalid value error. 2. Only works if we manipulate simple scalars (view data = model data). 3. Does not support multiple dependencies. 4. Does not support recursive dependencies. 2 dimensions problem: form lifecycle and time. 37 New wheel problems

Slide 38

Slide 38 text

Form lifecycle 38 Build Set Data Pre-submit & submit Post submit Type Present absent absent absent Data absent Model data Request & view data Model data Options read only read only read only read only Configuration Listeners, transformer, data-mapper, … Editable read only read only read only Children & parents Builder API Editable Editable read only PRE_SUBMIT SUBMIT POST_SUBMIT PRE_SET_DATA POST_SET_DATA BUILD Optional, on submit

Slide 39

Slide 39 text

The Form component uses the Composite design pattern: FormInterface + setParent(?self $parent) + getParent(): ?self + add(name, type, options) + get(name) / has(name) + remove(name) + all() 39 Form tree

Slide 40

Slide 40 text

SUBMIT SET_DATA BUILD 40 Recursive form life cycle Lifecycle Form tree root meal dish

Slide 41

Slide 41 text

SUBMIT SET_DATA BUILD 41 Recursive form life cycle SD SUBMIT SUBMIT SD Model data captured Lifecycle Form tree Build Build root meal dish

Slide 42

Slide 42 text

SUBMIT SET_DATA BUILD 42 Recursive form life cycle root meal dish SD SUBMIT SUBMIT SD Model data captured Lifecycle Form tree Build Build

Slide 43

Slide 43 text

SUBMIT SET_DATA BUILD 43 Recursive form life cycle root meal dish SD SUBMIT SUBMIT SD SD Build Build Model data captured Lifecycle Form tree Build

Slide 44

Slide 44 text

SUBMIT SET_DATA BUILD 44 Recursive form life cycle root meal dish SD SUBMIT SUBMIT SD SD Build Build Model data captured Extension points Lifecycle Form tree Build

Slide 45

Slide 45 text

45 Form translations

Slide 46

Slide 46 text

Integrated form translations 46 We want to normalize form translations.

Slide 47

Slide 47 text

47 Form extensions FormType FormTypeExtension getParent getExtendedTypes configureOptions buildForm buildView finishView

Slide 48

Slide 48 text

48 Form extensions CsrfTypeExtension ValidatorTypeExtension Form TranslationTypeExtension

Slide 49

Slide 49 text

Creating a custom type extension 49 namespace App\Form; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\ButtonType; use Symfony\Component\Form\Extension\Core\Type\FormType; class TranslationTypeExtension extends AbstractTypeExtension { public static function getExtendedTypes(): iterable { return [ FormType::class, ButtonType::class, ]; } }

Slide 50

Slide 50 text

class TranslationTypeExtension extends AbstractTypeExtension { // ... public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'translate_label' => true, 'translate_help' => false, 'translate_placeholder' => false, ]); $resolver->setAllowedTypes('translate_label', 'bool'); $resolver->setAllowedTypes('translate_help', 'bool'); $resolver->setAllowedTypes('translate_placeholder', 'bool'); } } 50 Options configuration

Slide 51

Slide 51 text

class TranslationTypeExtension extends AbstractTypeExtension { // ... public function buildView(FormView $view, FormInterface $form, array $options): void { // XXX: return if no option is enabled $current = $form; $label = []; while (null !== $current) { $name = $current->getName(); $label[] = $name; $current = $current->getParent(); } $prefix = implode('.', array_reverse($label)); } } 51 Compute the prefix for translation keys

Slide 52

Slide 52 text

class TranslationTypeExtension extends AbstractTypeExtension { // ... public function buildView(FormView $view, FormInterface $form, array $options): void { // ... if ($options['translate_label']) { $view->vars['label'] = $prefix.'.label'; } if ($options['translate_help']) { $view->vars['help'] = $prefix.'.help'; } if ($options['translate_placeholder']) { $view->vars['attr']['placeholder'] = $prefix.'.placeholder'; } $view->vars['translation_domain'] = 'form'; } } 52 Translation logic

Slide 53

Slide 53 text

class AccountCreateType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('firstName', TextType::class, [ 'translate_help' => true, 'translate_placeholder' => true, ]) ->add('lastName', TextType::class, [ 'label' => 'Nom', 'translate_placeholder' => true, ]) ; } } 53 Usage and control of the behavior

Slide 54

Slide 54 text

54 End result

Slide 55

Slide 55 text

55 Wrap-up We have covered: - Rich features out of the box ❤ - Types hierarchy - Form templating - Form lifecycle - Type extensions This should help you creating simple and maintainable form logic.

Slide 56

Slide 56 text

56 Thank you ❤