Symfony Forms 101

Symfony Forms 101

Symfony forms are so hard! That's what I hear a lot, but it's not quite true. If you stick to a set of best practices, Forms work wonderfully well.

Join me for this session to see the tricks that you should have known when you did your last project.

24d20907afea0f684e62d620b886af16?s=128

Bernhard Schussek

July 22, 2016
Tweet

Transcript

  1. 1.
  2. 2.

    Bernhard Schussek · webmozart.io 4/100 Bernhard Schussek Freelancer, Trainer and

    Coach Symfony Architecture Coding Practices webmozart.io
  3. 12.

    Bernhard Schussek · webmozart.io 14/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() FormBuilder Form Order FormView update validate()
  4. 13.

    Bernhard Schussek · webmozart.io 15/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  5. 14.

    Bernhard Schussek · webmozart.io 16/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  6. 15.

    Bernhard Schussek · webmozart.io 17/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  7. 16.

    Bernhard Schussek · webmozart.io 18/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  8. 17.

    Bernhard Schussek · webmozart.io 19/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  9. 18.

    Bernhard Schussek · webmozart.io 20/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  10. 20.

    Bernhard Schussek · webmozart.io 22/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  11. 22.

    Bernhard Schussek · webmozart.io 24/100 $form = $this->createForm(PaymentType::class, $order, [

    'action' => $this->generateUrl('payment_gw'), ]); ✓ Use "action" option in the controller
  12. 23.

    Bernhard Schussek · webmozart.io 25/100 $form = $this->createForm(PaymentType::class, $order, [

    'method' => 'PUT', ]); ✓ Use "method" option in the controller
  13. 24.

    Bernhard Schussek · webmozart.io 26/100 $defaultShipping = $this->getDefaultShipping($customer); $form =

    $this->createForm( new PlaceOrderType($defaultShipping), ); ✗ Don't Pass Dynamic Data to Constructor
  14. 25.

    Bernhard Schussek · webmozart.io 27/100 $defaultShipping = $this->getDefaultShipping($customer); $form =

    $this->createForm(PlaceOrderType::class, .., [ 'default_shipping' => $defaultShipping, ]); ✓ Use Custom Options
  15. 26.

    Bernhard Schussek · webmozart.io 28/100 parameters: app.us_shipping.enabled: true services: app.form.shipping:

    class: AppBundle\Form\ShippingType arguments: - %app.us_shipping.enabled% tags: - { type: form.type } ✓ Pass Global Settings To Constructor
  16. 27.

    Bernhard Schussek · webmozart.io 29/100 class ShippingType extends AbstractType {

    private $usShipping; public function __construct($usShipping) { $this->usShipping = (bool) $usShipping; } // ... } ✓ Pass Global Settings To Constructor
  17. 28.

    Bernhard Schussek · webmozart.io 30/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  18. 30.

    Bernhard Schussek · webmozart.io 32/100 {{ form_row(form.firstName) }} {{ form_row(form.lastName)

    }} {{ form_row(form.address) }} {{ form_row(form.submit) }} ✓ Use form_row() to Control Field Order
  19. 31.

    Bernhard Schussek · webmozart.io 33/100 {{ form_row(form.address, { 'label': 'Your

    Address', 'attr': {'class': 'shippable-address'}, 'label_attr': {'data-id': 5} }) }} ✓ Set Labels/Attributes in the Template
  20. 33.

    Bernhard Schussek · webmozart.io 35/100 <div class="form-group"> {{ form_errors(form.paymentMethod) }}

    <label for="{{ form.paymentMethod.vars.id }}"> Payment Method {{ form_widget(form.paymentMethod) }} </label> </div> ✓ Write Custom HTML By Hand
  21. 36.

    Bernhard Schussek · webmozart.io 38/100 public function buildForm($builder, array $options)

    { $fields = $this->fieldRepository->findAll(); $fieldsForm = $builder->create('fields', FormType::c foreach ($fields as $field) { $fieldsForm->add($field->getName(), $field->getT } $builder->add($fieldsForm); }
  22. 37.

    Bernhard Schussek · webmozart.io 39/100 public function finishView($view, $form) {

    $fieldViews = $view->children['fields']; $tabs = $this->tabRespository->findAll(); foreach ($tabs as $tab) { $tabView = new FormView(); $tabView->vars['label'] = $tab->getName() // reorganize field views... $view->children['tabs'][] = $tabView; } } ✓ Move Complex Logic to build/finishView()
  23. 38.

    Bernhard Schussek · webmozart.io 40/100 {% for tab in form.tabs

    %} {{ tab.vars.label }} {% for field in tab %} {{ form_row(field) }} {% endfor %} {% endfor %}
  24. 39.

    Bernhard Schussek · webmozart.io 41/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  25. 42.
  26. 43.

    Bernhard Schussek · webmozart.io 46/100 /** * @Callback */ public

    function validateTotal( ExecutionContextInterface $context) { if ($this->total !== $this->getProductsTotal()) { $this->context->addViolation('Invalid.'); } } ✓ Use the @Callback Constraint
  27. 46.

    Bernhard Schussek · webmozart.io 49/100 /** * @NotNull * @GreaterThan(0,

    groups="Checkout") */ private $total; ✓ Use Validation Groups for Partial Validation
  28. 47.

    Bernhard Schussek · webmozart.io 50/100 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'validation_groups' => new GroupSequence([ 'Default', 'VatCheck' ]), ]); } ✓ Use Group Sequences for Sequential Validation
  29. 48.

    Bernhard Schussek · webmozart.io 51/100 class ProductName extends Regex {

    public function __construct($options = []) { parent::__construct(array_replace([ 'pattern' => '/^\w.*$/' ], $options); } public function validatedBy() { return RegexValidator::class; } } ✓ Create Domain-Specific Constraints
  30. 49.

    Bernhard Schussek · webmozart.io 52/100 class ProductNameValidator extends ConstraintValidator {

    public function validated($value, Constraint $constr { $this->context->getValidator() ->inContext($this->context) ->validate($value, new Regex(self::PATTERN)) ->validate($value, new NotEqualTo('keyword') ; } } ✓ Create Composite Constraints
  31. 50.

    Bernhard Schussek · webmozart.io 53/100 class ProductNameValidator extends ConstraintValidator {

    public function validated($value, Constraint $constr { if (null === $value) { return; } // ... } } ✓ Ignore Null Values
  32. 51.

    Bernhard Schussek · webmozart.io 54/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  33. 52.

    Bernhard Schussek · webmozart.io 55/100 Clark Kent ... Submit Name

    VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3
  34. 53.

    Bernhard Schussek · webmozart.io 56/100 $builder->add('vat', TextType::class, [ 'property_path' =>

    'customer.vatId' ]); ✓ Decouple Forms from the Model Structure
  35. 55.

    Bernhard Schussek · webmozart.io 58/100 class Customer { public function

    __construct($name, Address $address) { // ... } } What About Non-Empty Constructors?
  36. 56.

    Bernhard Schussek · webmozart.io 59/100 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'empty_data' => function (FormInterface $form) { return new Customer( $form->get('name')->getData(), $form->get('address')->getData() ); }, ]); } ✓ Use "empty_data" for Non-Empty Constructors
  37. 57.

    Bernhard Schussek · webmozart.io 60/100 class Money { public function

    __construct($amount) { $this->amount = $amount; } public function getAmount() { return $this->amount; } } What About Value Objects?
  38. 60.

    Bernhard Schussek · webmozart.io 63/100 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { public function buildForm( FormBuilderInterface $builder, array $options) { $builder->addModelTransformer($this); } public function getParent() { return MoneyType::class; } // ... } ✓ Use Data Transformers (1/3)
  39. 61.

    Bernhard Schussek · webmozart.io 64/100 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { // ... public function transform($money) { if (null === $money) { return null; } return $money->getAmount(); } // ... } ✓ Use Data Transformers (2/3)
  40. 62.

    Bernhard Schussek · webmozart.io 65/100 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { // ... public function reverseTransform($amount) { if (null === $amount) { return null; } return new Money($amount); } } ✓ Use Data Transformers (3/3)
  41. 63.

    Bernhard Schussek · webmozart.io 66/100 class Customer { public function

    relocate(Address $address) { // ... } } What About Non-Standard Methods?
  42. 64.

    Bernhard Schussek · webmozart.io 67/100 Clark Kent ... Submit Name

    VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3
  43. 65.

    Bernhard Schussek · webmozart.io 68/100 class CustomerType extends AbstractType implements

    DataMapperInterface { public function buildForm( FormBuilderInterface $builder, array $options) { $builder->setDataMapper($this); } // ... } ✓ Use Data Mappers (1/3)
  44. 66.

    Bernhard Schussek · webmozart.io 69/100 class CustomerType extends AbstractType implements

    DataMapperInterface { // ... public function mapDataToForms($customer, $forms) { $forms = iterator_to_array($forms); $forms['address']->setData($customer->getAddress // ... } // ... } ✓ Use Data Mappers (2/3)
  45. 67.

    Bernhard Schussek · webmozart.io 70/100 class CustomerType extends AbstractType implements

    DataMapperInterface { // ... public function mapFormsToData($forms, &$customer) { $forms = iterator_to_array($forms); $customer->relocate($forms['address']->getData() // ... } // ... } ✓ Use Data Mappers (3/3)
  46. 68.

    Bernhard Schussek · webmozart.io 75/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  47. 69.

    Bernhard Schussek · webmozart.io 76/100 $builder->add('expressShipping', ChoiceType::class, [ 'choices' =>

    [ 'Decide later' => Selection::get(LATER), 'No' => Selection::get(NO), 'Yes' => Selection::get(YES), ], 'choices_as_values' => true, ]); ✓ Learn the Options of ChoiceType
  48. 70.

    Bernhard Schussek · webmozart.io 77/100 $builder->add('expressShipping', ChoiceType::class, [ // ...

    // $shipping->getTitle() 'choice_label' => 'title', ]); <select> <option value="0">Decide later</option> <option value="1">No</option> <option value="2">Yes</option> </select>
  49. 71.

    Bernhard Schussek · webmozart.io 78/100 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_label' => function (Selection $selection) { return humanize($selection->getType()); }, ]); <select> <option value="0">Decide later</option> <option value="1">No</option> <option value="2">Yes</option> </select>
  50. 72.

    Bernhard Schussek · webmozart.io 79/100 $builder->add('expressShipping', ChoiceType::class, [ // ...

    // $selection->getType() 'choice_value' => 'type' ]); <select> <option value="later">Decide later</option> <option value="no">No</option> <option value="yes">Yes</option> </select>
  51. 73.

    Bernhard Schussek · webmozart.io 80/100 $builder->add('paymentMethod', ChoiceType::class, [ // ...

    // $paymentMethod->getCategory()->getName() 'group_by' => 'category.name', ]); <select> <optgroup label="Credit Card"> ... </optgroup> ... </select>
  52. 74.

    Bernhard Schussek · webmozart.io 81/100 $builder ->add('country', 'entity', [ ...

    ]) ->add('province', 'entity', [ ... ]) ; What About Field Dependencies?
  53. 76.

    Bernhard Schussek · webmozart.io 83/100 $addProvinceField = function (FormEvent $event)

    { $form = $event->getForm()->getParent(); $country = $event->getData(); $form->add('province', EntityType::class, [ 'class' => Province::class, 'query_builder' => function ($repo) use ($countr // query provinces by $country }, ]); }; ✓ Use POST_* Hooks On Fields (2/2)
  54. 78.

    Bernhard Schussek · webmozart.io 85/100 class Contact { private $firstName;

    private $lastName; private $email; private $phoneNumber; private $address; // accessor methods... } Example: Contact Details
  55. 79.

    Bernhard Schussek · webmozart.io 86/100 $contact = $this->contactRepository->get($contactId); $form =

    $this->createForm(ContactType::class, $contact); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->contactRepository->flush($contact); } ✓ Easy to Fetch/Update
  56. 80.

    Bernhard Schussek · webmozart.io 87/100 class Contact { // ...

    public function setAddress(Address $address) { $this->address = $address; } } ✓ Write Strict Method Signatures
  57. 81.

    Bernhard Schussek · webmozart.io 88/100 class ContactType extends AbstractType {

    public function buildForm($builder, array $options) { // ... $builder->add('address', AddressType::class, [ 'constraints' => new NotNull(), ]); } } ✓ Place Validation Constraints in the Form
  58. 82.

    Bernhard Schussek · webmozart.io 89/100 class ContactType extends AbstractType implements

    DataMapperInterface { public function buildForm($builder, array $options) { $builder->setDataMapper($this); } // ... } ✓ Write Your Own Data Mapper
  59. 83.

    Bernhard Schussek · webmozart.io 90/100 class ContactType implements DataMapperInterface {

    // ... public function mapDataToForms($data, $forms) { $forms = iterator_to_array($forms); if (!$data instanceof Contact) { return null; } $forms['firstName']->setData($data->getFirstName $forms['lastName']->setData($data->getLastName() $forms['email']->setData($data->getEmail()); $forms['phoneNumber']->setData($data->getPhoneNu $forms['address']->setData($data->getAddress()); } // ... }
  60. 84.

    Bernhard Schussek · webmozart.io 91/100 class ContactType implements DataMapperInterface {

    // ... public function mapFormsToData($forms, &$data) { $forms = iterator_to_array($forms); if (!$data instanceof Contact) { $data = new Contact(); } $data->setFirstName($forms['firstName']->getData $data->setLastName($forms['lastName']->getData() $data->setEmail($forms['email']->getData()); $data->setPhoneNumber($forms['phoneNumber']->get if (null !== $forms['address']->getData()) { $data->setAddress($forms['address']->getData } } }
  61. 88.

    Bernhard Schussek · webmozart.io 95/100 class Contact { ... }

    Aggregate Root class ContactCommandHandler { ... } Command Handler class ModifyContact { ... } Command
  62. 90.

    Bernhard Schussek · webmozart.io 97/100 public function handleModifyContact(ModifyContact $comma $contact

    = $this->repo->get($command->getContactId() $fields = $this->fieldRepo->getMany( array_keys($command->getFieldValues()) ); $contact->modify($fields, $command->getFieldValues() $this->repo->flush(); } ✓ Handle in the Command Handler
  63. 91.

    Bernhard Schussek · webmozart.io 98/100 class Contact { public function

    modify(array $fields, array $values) { foreach ($values as $fieldName => $value) { if (!$fields[$fieldName]->accepts($value)) { throw new Exception(); } } // ... } } ✓ Aggregate Roots Control Business Rules
  64. 93.

    Bernhard Schussek · webmozart.io 100/100 public function mapDataToForms($data, $forms) {

    $forms = iterator_to_array($forms); if (!$data instanceof Contact) { return; } $forms['fieldValues']->setData($data->getFieldValues } ✓ Map Aggregates (CQRS: View Models) to Forms
  65. 94.

    Bernhard Schussek · webmozart.io 101/100 public function mapFormsToData($forms, &$data) {

    $forms = iterator_to_array($forms); $data = new ModifyContact( $forms['fieldValues']->getData() ); } ✓ Map Aggregates to Forms