Symfony CoP: Form component

511 views

Published on

Slides about Symfony Form component at Inviqa's Symfony community of practice.

Published in: Software
  • Be the first to comment

Symfony CoP: Form component

  1. 1. Symfony Form component
  2. 2. Plan 1. Basic usage 2. Validation 3. Custom types 4. Events 5. Data Transformers 6. Form type extensions 7. Rendering overview
  3. 3. What's the Form component?
  4. 4. Basic usages
  5. 5. Basic usage (Symfony way) public function createAction(Request $request) { $form = $this ->createFormBuilder([]) ->add('comment', 'textarea') ->getForm() ; $form->handleRequest($request); if ($form->isValid()) { $data = $form->getData(); // Do what ever you want with the data... $comment = $data['comment']; } return [ 'form' => $form->createView(), ]; }
  6. 6. Basic usage (rendering) # create.html.twig {{ form_start(form) }} {{ form_errors(form) }} {{ form_row(form.comment) }} <input type="submit" name="Let's go" /> {{ form_end(form) }}
  7. 7. Basic usage (Object instead of array) public function createOrUpdateAction(Request $request, MyObject $myObject = null) { $myObject = $myObject :? new MyObject(); $form = $this ->createFormBuilder($myObject, [ 'data_class' => MyObject::class, ]) ->add('comment', 'textarea') ->getForm() ; $form->handleRequest($request); if ($form->isValid()) { // Do what ever you want with the updated object... $comment = $myObject->getComment(); } return [ 'form' => $form->createView(), ]; }
  8. 8. Validation
  9. 9. Validation $form = $this->createFormBuilder() ->add('comment', 'textarea', [ 'required' => true, 'constraints' => [ new NotBlank(), ], ]) ->getForm() ;
  10. 10. Basic usage (Validation on the object) use SymfonyComponentValidatorConstraints as Assert; class MyObject { /** * @AssertNotBlank * @AssertLength( * min=10, * minMessage="The comment have to be useful" * ) */ private $comment; // Required methods public function getComment(); public function setComment($comment); }
  11. 11. Basic usage (Get validation errors) ...that's rendered for you! But if you want access to them... $form->handleRequest($request); if (!$form->isValid()) { $errors = form->getErrors(); // `$errors` is an array of `FormError` objects. }
  12. 12. Hey! 31 built-in types 46 built-in validators
  13. 13. Custom types
  14. 14. Custom type definition class MyFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('comment', 'textarea') ; } }
  15. 15. Using the Form Type public function createAction(Request $request) { $form = $this->createForm(new MyFormType()); $form->handleRequest($request); if ($form->isValid()) { $data = $form->getData(); // Do what ever you want with the data... $comment = $data['comment']; } return [ 'form' => $form->createView(), ]; }
  16. 16. Form Type options class MyFormType extends AbstractType { //! Replace `setDefaultOptions` since Symfony 3.0 /! public function configureOptions(OptionsResolver $resolver) { $resolver->setRequired(['my-custom-option']); $resolver->setDefaults(array( 'data_class' => 'AppModelObject', )); } }
  17. 17. Form Type as a service class GenderType extends AbstractType { private $genderChoices; public function __construct(array $genderChoices) { $this->genderChoices = $genderChoices; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'choices' => $this->genderChoices, )); } }
  18. 18. Form Type as a service Register the form type <service id="app.form.type.gender" class="AppFormTypeGenderType"> <argument>%genders%</argument> <tag name="form.type" /> </service> Use the form type class MyFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('gender', GenderType::class) ; } }
  19. 19. Events Or how to dynamically update the FormType based on data.
  20. 20. Form workflow It dispatches different events while handling the requests. » PRE_SET_DATA » POST_SET_DATA » PRE_SUBMIT » SUBMIT » POST_SUBMIT
  21. 21. The name field only for a new product public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { $product = $event->getData(); $form = $event->getForm(); // The product name is only updatable for a new `Product` if (!$product || null === $product->getId()) { $form->add('name', TextType::class); } }); }
  22. 22. Event subscribers public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->addEventSubscriber(new AddNameFieldSubscriber()); }
  23. 23.  An event subscriber class AddNameFieldSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return array(FormEvents::PRE_SET_DATA => 'preSetData'); } public function preSetData(FormEvent $event) { $product = $event->getData(); $form = $event->getForm(); if (!$product || null === $product->getId()) { $form->add('name', TextType::class); } } }
  24. 24. List based on another field 1. Initial Form Type public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('sport', EntityType::class, array( 'class' => 'AppBundle:Sport', 'placeholder' => '', )) ; }
  25. 25. List based on another field 2. Our form modifier $formModifier = function (FormInterface $form, Sport $sport = null) { $positions = null === $sport ? array() : $sport->getAvailablePositions(); $form->add('position', EntityType::class, array( 'class' => 'AppBundle:Position', 'placeholder' => '', 'choices' => $positions, )); };
  26. 26. List based on another field 3. The listeners $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($formModifier) { $data = $event->getData(); $formModifier($event->getForm(), $data->getSport()); }); $builder->get('sport')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($formModifier) { // It's important here to fetch $event->getForm()->getData(), as // $event->getData() will get you the client data (that is, the ID) $sport = $event->getForm()->getData(); // since we've added the listener to the child, we'll have to pass on // the parent to the callback functions! $formModifier($event->getForm()->getParent(), $sport); });
  27. 27. Data Transformers
  28. 28. Normalization flow 1. Model data. Our object returned by getData(). 2. Internal representation, mostly our model data. Almost never used by the developer. 3. View data. The data structure sent to submit().
  29. 29. Using the CallbackTransformer public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('description', TextareaType::class); $builder->get('description')->addModelTransformer(new CallbackTransformer(function ($originalDescription) { // transform <br/> to n so the textarea reads easier return preg_replace('#<brs*/?>#i', "n", $originalDescription); }, function ($submittedDescription) { // remove most HTML tags (but not br,p) $cleaned = strip_tags($submittedDescription, '<br><br/><p>'); // transform any n to real <br/> return str_replace("n", '<br/>', $cleaned); })); }
  30. 30. An integer to an object 1. Our task Form Type class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('description', TextareaType::class) ->add('issue', TextType::class) ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppModelTask' )); } // ... }
  31. 31. An integer to an object 2. The transformer class IssueToNumberTransformer implements DataTransformerInterface { private $issueRepository; public function __construct(IssueRepository $issueRepository) { $this->issueRepository = $issueRepository; } // Two methods to implement... public function transform($issue); public function reverseTransform($issueNumber); }
  32. 32. An integer to an object 3. From model to view public function transform($issue) { return null !== $issue ? $issue->getId() : ''; }
  33. 33. An integer to an object 3. From view to model public function reverseTransform($issueNumber) { if (empty($issueNumber)) { return; } if (null === ($issue = $this->issueRepository->find($issueNumber))) { throw new TransformationFailedException(sprintf('An issue with number "%s" does not exist!', $issueNumber)); } return $issue; }
  34. 34. An integer to an object 4. Voilà! class TaskType extends AbstractType { private $issueRepository; public function __construct(IssueRepository $issueRepository) { $this->issueRepository = $issueRepository; } public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->get('issue')->addModelTransformer(new IssueToNumberTransformer($this->issueRepository)); } }
  35. 35. Form Type Extensions
  36. 36. An extension class class IconTypeExtension extends AbstractTypeExtension { public function getExtendedType() { return ChoiceType::class; } // We can now declare the following methods... public function configureOptions(OptionsResolver $resolver); public function buildForm(FormBuilderInterface $builder, array $options); public function buildView(FormView $view, FormInterface $form, array $options); public function finishView(FormView $view, FormInterface $form, array $options) }
  37. 37. An optional extra icon class IconTypeExtension extends AbstractTypeExtension { public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'icon' => null, ]); } }
  38. 38. An optional extra icon class IconTypeExtension extends AbstractTypeExtension { public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['icon'] = $options['icon']; } } You'll need to extend the choice_widget block using a form theme, so you can display an icon when the local icon variable is defined.
  39. 39. Register the extension <service id="app.image_type_extension" class="AppFormExtensionImageTypeExtension"> <tag name="form.type_extension" extended-type="SymfonyComponentFormExtensionCoreTypeChoiceType" /> </service>
  40. 40.  Rendering
  41. 41. Using a form theme {% form_theme form 'form/fields.html.twig' %} {# or.. #} {% form_theme form _self %} {{ form(form) }}
  42. 42. Example theme # form/fields.html.twig {% block form_row %} {% spaceless %} <div class="form_row"> {{ form_label(form) }} {{ form_errors(form) }} {{ form_widget(form) }} </div> {% endspaceless %} {% endblock form_row %}
  43. 43. Creating a form theme Using Twig blocks. » [type]_row » [type]_widget » [type]_label » [type]_errors If the given block of type is not found, it will use the parent's type.
  44. 44. FormType's buildView class GenderType extends AbstractType { private $genderChoices; public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['genders'] = $this->genderChoices; } }
  45. 45. FormType's buildView # form/fields.html.twig {% block gender_widget %} {% spaceless %} {# We can use the `gender` variable #} {% endspaceless %} {% endblock %}
  46. 46. FormType's finishView Called when the children's view is completed. public function finishView(FormView $view, FormInterface $form, array $options) { $multipart = false; foreach ($view->children as $child) { if ($child->vars['multipart']) { $multipart = true; break; } } $view->vars['multipart'] = $multipart; }
  47. 47. Creating a form without name 1. The difference? Your form properties won't be namespaced Example: comment instead of my_form[comment] You might need/want to use it for: - Legacy applications compatibility - APIs (with the FOS Rest Body Listener)
  48. 48. Creating a form without name 2. How? private $formFactory; public function __construct(FormFactoryInterface $formFactory) { $this->formFactory = $formFactory; } public function createAction(Request $request) { $form = $this->formFactory->createNamed(null, new MyFormType()); $form->handleRequest($request); // ... }
  49. 49. Wow, we're done!

×