Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Symfony Form - Practical use cases

Symfony Form - Practical use cases

Alexandre Salomé

January 13, 2025
Tweet

More Decks by Alexandre Salomé

Other Decks in Programming

Transcript

  1. 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
  2. 5

  3. 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
  4. New use cases In this new edition: 1. Creating a

    form 2. Required red asterisk 3. Dependent form fields 4. Form translations 7
  5. 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
  6. 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
  7. 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
  8. #[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
  9. // 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
  10. // 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
  11. {# templates/components/RegisterInformationForm.html.twig #} <div {{ attributes }}> {{ form(form, {attr:

    { Novalidate:'novalidate', 'data-action': 'live#action:prevent', 'data-live-action-param': 'saveForm', }}) }} </div> 15 Hello component - Twig
  12. 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.
  13. 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.
  14. {# 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
  15. 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
  16. 20 Feature-complete forms - CSS frameworks templates - Security &

    validation - Live components Ajax validation ❤ Dynamic forms ❤
  17. 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 %}
  18. ROW WIDGET 27 Form templates ERRORS LABEL ROW WIDGET ERRORS

    LABEL ROW WIDGET ERRORS LABEL ROW WIDGET ERRORS LABEL
  19. 28 Implementing the red asterisk {# templates/form.html.twig #} {% use

    "bootstrap_5_layout.html.twig" %} {% block form_label_content %} {{- parent() -}} {% if required %} <span class="text-danger"> *</span> {% endif %} {% endblock %}
  20. 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!
  21. // 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
  22. $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
  23. $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
  24. 36

  25. 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
  26. 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
  27. 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
  28. SUBMIT SET_DATA BUILD 41 Recursive form life cycle SD SUBMIT

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

    dish SD SUBMIT SUBMIT SD Model data captured Lifecycle Form tree Build Build
  30. 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
  31. 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
  32. 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, ]; } }
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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.