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

Symfony2 Forms: Do's and Don'ts

Symfony2 Forms: Do's and Don'ts

The Symfony Form component is one of the most complex components of the whole framework and often leaves developers startled. Let me give you a few guidelines that make development of forms much easier than you would have thought.

Bernhard Schussek

April 29, 2016
Tweet

More Decks by Bernhard Schussek

Other Decks in Programming

Transcript

  1. Bernhard Schussek · webmozart.io 2/122 http://slightlyviral.com/23-examples-of-why-women-live-longer-than-men/ Symfony2 Forms Symfony2 Forms

    Dos And Don'ts Dos And Don'ts Bernhard Schussek (@webmozart) Bernhard Schussek (@webmozart) Symfony Live Köln 2015 Symfony Live Köln 2015
  2. Bernhard Schussek · webmozart.io 5/122 Bernhard Schussek Freelancer, Trainer and

    Coach Symfony Architecture Coding Practices webmozart.io
  3. Bernhard Schussek · webmozart.io 18/122 $builder ->add('firstName', 'text') ->add('lastName', 'text')

    ->add('address', 'app_address') ->add('shippingMethod', 'entity', [ // ... ]) ;
  4. Bernhard Schussek · webmozart.io 19/122 $builder ->add('firstName', TextType::class) ->add('lastName', TextType::class)

    ->add('address', AddressType::class) ->add('shippingMethod', EntityType::class, [ // ... ]) ;
  5. Bernhard Schussek · webmozart.io 22/122 class PlaceOrderType extends AbstractType {

    public function buildForm(FormBuilderInterface $buil { // ... } public function getName() { return 'app_place_order'; } }
  6. Bernhard Schussek · webmozart.io 23/122 class PlaceOrderType extends AbstractType {

    public function buildForm(FormBuilderInterface $buil { // ... } }
  7. Bernhard Schussek · webmozart.io 24/122 app.form.place_order: class: AppBundle\Form\PlaceOrderType tags: -

    { name: form.type, alias: app_place_order } app.form.submit_payment: class: AppBundle\Form\SubmitPaymentType tags: - { name: form.type, alias: app_submit_payment }
  8. Bernhard Schussek · webmozart.io 25/122 app.form.process_order: class: AppBundle\Form\ProcessOrderType arguments: -

    @app.shipping_service tags: - { name: form.type, alias: app_process_order }
  9. Bernhard Schussek · webmozart.io 27/122 {% block app_place_order_widget %} {#

    ... #} {% endblock %} class PlaceOrderType extends AbstractType { // ... }
  10. Bernhard Schussek · webmozart.io 28/122 {% block place_order_widget %} {#

    ... #} {% endblock %} class PlaceOrderType extends AbstractType { // ... }
  11. Bernhard Schussek · webmozart.io 29/122 {% block app_place_order_widget %} {#

    ... #} {% endblock %} class PlaceOrderType extends AbstractType { // ... public function getBlockPrefix() { return 'app_place_order'; } }
  12. Bernhard Schussek · webmozart.io 31/122 class PlaceOrderType extends AbstractType {

    public function setDefaultOptions( OptionsResolverInterface $resolver) { // ... } }
  13. Bernhard Schussek · webmozart.io 32/122 class PlaceOrderType extends AbstractType {

    public function configureOptions( OptionsResolver $resolver) { // ... } }
  14. Bernhard Schussek · webmozart.io 35/122 $builder->add('lineItems', CollectionType::class, [ 'type' =>

    OrderLineType::class, 'prototype_data' => new OrderLine( 'New item', 120.00 ), ]);
  15. Bernhard Schussek · webmozart.io 37/122 $builder->add('shippingMethod', EntityType::class, [ 'class' =>

    ShippingMethod::class, 'query_builder' => function ($repo) use ($address) { if (!$address) { return null; } return $repo->queryByAddress($address); ), ]);
  16. Bernhard Schussek · webmozart.io 41/122 $builder->add('expressShipping', ChoiceType::class, [ 'choices' =>

    [ 'Decide later' => Selection::get(LATER), 'No' => Selection::get(NO), 'Yes' => Selection::get(YES), ], ]);
  17. Bernhard Schussek · webmozart.io 42/122 $builder->add('expressShipping', ChoiceType::class, [ 'choices' =>

    [ 'Decide later' => Selection::get(LATER), 'No' => Selection::get(NO), 'Yes' => Selection::get(YES), ], 'choices_as_values' => true, ]); 2.8 only
  18. Bernhard Schussek · webmozart.io 44/122 $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>
  19. Bernhard Schussek · webmozart.io 45/122 $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>
  20. Bernhard Schussek · webmozart.io 47/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    ]); <select> <option value="0">Decide later</option> <option value="1">No</option> <option value="2">Yes</option> </select>
  21. Bernhard Schussek · webmozart.io 48/122 $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>
  22. Bernhard Schussek · webmozart.io 49/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_value' => function (Selection $selection) { return strtoupper($selection->getType()); }, ]); <select> <option value="LATER">Decide later</option> <option value="NO">No</option> <option value="YES">Yes</option> </select>
  23. Bernhard Schussek · webmozart.io 51/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_attr' => ['data-reference' => 1], ]); <select> <option data-reference="1">Decide later</option> <option data-reference="1">No</option> <option data-reference="1">Yes</option> </select>
  24. Bernhard Schussek · webmozart.io 52/122 $builder->add('expressShipping', ChoiceType::class, [ // ...

    'choice_attr' => function (Selection $selection) { return ['data-reference' => $selection->getRef() }, ]); <select> <option data-reference="1">Decide later</option> <option data-reference="2">No</option> <option data-reference="3">Yes</option> </select>
  25. Bernhard Schussek · webmozart.io 54/122 $builder->add('paymentMethod', ChoiceType::class, [ 'choices' =>

    [ 'Credit Card' => [ 'VISA' => PaymentMethod::VISA, 'MasterCard' => PaymentMethod::MASTER_CARD, ], 'Bank Transfer' => [ // ... ], ], ]); <select> <optgroup label="Credit Card"> ... </optgroup> ... </select>
  26. Bernhard Schussek · webmozart.io 55/122 $builder->add('paymentMethod', ChoiceType::class, [ // ...

    // $paymentMethod->getCategory() 'group_by' => 'category', ]); <select> <optgroup label="Credit Card"> ... </optgroup> ... </select>
  27. Bernhard Schussek · webmozart.io 56/122 $builder->add('paymentMethod', ChoiceType::class, [ // ...

    // $paymentMethod->getCategory()->getName() 'group_by' => 'category.name', ]); <select> <optgroup label="Credit Card"> ... </optgroup> ... </select>
  28. Bernhard Schussek · webmozart.io 57/122 $builder->add('paymentMethod', ChoiceType::class, [ // ...

    'group_by' => function (PaymentMethod $method) { return sprintf( '%s (%s)', $method->getCategory()->getName(), $method->getCategory()->getFees() ); }, ]); <select> <optgroup label="Credit Card (5%)"> ... </optgroup> ... </select>
  29. Bernhard Schussek · webmozart.io 60/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() FormBuilder Form Order FormView update validate()
  30. Bernhard Schussek · webmozart.io 61/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  31. Bernhard Schussek · webmozart.io 62/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  32. Bernhard Schussek · webmozart.io 63/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  33. Bernhard Schussek · webmozart.io 64/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  34. Bernhard Schussek · webmozart.io 65/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  35. Bernhard Schussek · webmozart.io 66/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  36. Bernhard Schussek · webmozart.io 68/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  37. Bernhard Schussek · webmozart.io 69/122 Where to Create Forms? In

    the Controller Custom Form Type Custom Form Type
  38. Bernhard Schussek · webmozart.io 73/122 $form = $this->createForm(PaymentType::class, $order, [

    'action' => $this->generateUrl('payment_gw'), ]); ✓ Use "action" option in the controller
  39. Bernhard Schussek · webmozart.io 74/122 $form = $this->createForm(PaymentType::class, $order, [

    'method' => 'PUT', ]); ✓ Use "method" option in the controller
  40. Bernhard Schussek · webmozart.io 75/122 $defaultShipping = $this->getDefaultShipping($customer); $form =

    $this->createForm( new PlaceOrderType($defaultShipping), ); ✗ Don't Pass Dynamic Data to Constructor
  41. Bernhard Schussek · webmozart.io 76/122 $defaultShipping = $this->getDefaultShipping($customer); $form =

    $this->createForm(PlaceOrderType::class, .., [ 'default_shipping' => $defaultShipping, ]); ✓ Use Custom Options
  42. Bernhard Schussek · webmozart.io 77/122 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
  43. Bernhard Schussek · webmozart.io 78/122 class ShippingType extends AbstractType {

    private $usShipping; public function __construct($usShipping) { $this->usShipping = (bool) $usShipping; } // ... } ✓ Pass Global Settings To Constructor
  44. Bernhard Schussek · webmozart.io 79/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  45. Bernhard Schussek · webmozart.io 81/122 {{ form_row(form.firstName) }} {{ form_row(form.lastName)

    }} {{ form_row(form.address) }} {{ form_row(form.submit) }} ✓ Use form_row() to Control Field Order
  46. Bernhard Schussek · webmozart.io 82/122 {{ form_row(form.address, { 'label': 'Your

    Address', 'attr': {'class': 'shippable-address'}, 'label_attr': {'data-id': 5} }) }} ✓ Set Labels/Attributes in the View
  47. Bernhard Schussek · webmozart.io 84/122 <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
  48. Bernhard Schussek · webmozart.io 86/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  49. Bernhard Schussek · webmozart.io 90/122 /** * @NotNull * @Expression("value

    < this.getThreshold()") */ private $memory; ✓ Use the @Expression Constraint
  50. Bernhard Schussek · webmozart.io 91/122 /** * @Callback */ public

    function validateTotal( ExecutionContextInterface $context) { if ($this->total !== $this->getProductsTotal()) { $this->context->addViolation('Invalid.'); } } ✓ Use the @Callback Constraint
  51. Bernhard Schussek · webmozart.io 94/122 /** * @NotNull * @GreaterThan(0,

    groups="Checkout") */ private $total; ✓ Use Validation Groups for Partial Validation
  52. Bernhard Schussek · webmozart.io 95/122 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'validation_groups' => new GroupSequence([ 'Default', 'VatCheck' ]), ]); } ✓ Use Group Sequences for Sequential Validation
  53. Bernhard Schussek · webmozart.io 96/122 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
  54. Bernhard Schussek · webmozart.io 97/122 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
  55. Bernhard Schussek · webmozart.io 98/122 class ProductNameValidator extends ConstraintValidator {

    public function validated($value, Constraint $constr { if (null === $value) { return; } // ... } } ✓ Ignore Null Values
  56. Bernhard Schussek · webmozart.io 99/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  57. Bernhard Schussek · webmozart.io 100/122 Clark Kent ... Submit Name

    VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3
  58. Bernhard Schussek · webmozart.io 101/122 $builder->add('vat', TextType::class, [ 'property_path' =>

    'customer.vatId' ]); ✓ Decouple Forms from the Model Structure
  59. Bernhard Schussek · webmozart.io 103/122 class Customer { public function

    __construct($name, Address $address) { // ... } } What About Non-Empty Constructors?
  60. Bernhard Schussek · webmozart.io 104/122 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
  61. Bernhard Schussek · webmozart.io 105/122 class Money { public function

    __construct($amount) { $this->amount = $amount; } public function getAmount() { return $this->amount; } } What About Value Objects?
  62. Bernhard Schussek · webmozart.io 108/122 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)
  63. Bernhard Schussek · webmozart.io 109/122 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { // ... public function transform($money) { if (null === $money) { return null; } return $money->getAmount(); } // ... } ✓ Use Data Transformers (2/3)
  64. Bernhard Schussek · webmozart.io 110/122 class MyMoneyType extends AbstractType implements

    DataTransformerInterface { // ... public function reverseTransform($amount) { if (null === $amount) { return null; } return new Money($amount); } } ✓ Use Data Transformers (3/3)
  65. Bernhard Schussek · webmozart.io 111/122 class Customer { public function

    relocate(Address $address) { // ... } } What About Non-Standard Methods?
  66. Bernhard Schussek · webmozart.io 112/122 Clark Kent ... Submit Name

    VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3
  67. Bernhard Schussek · webmozart.io 113/122 class CustomerType extends AbstractType implements

    DataMapperInterface { public function buildForm( FormBuilderInterface $builder, array $options) { $builder->setDataMapper($this); } // ... } ✓ Use Data Mappers (1/3)
  68. Bernhard Schussek · webmozart.io 114/122 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)
  69. Bernhard Schussek · webmozart.io 115/122 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)
  70. Bernhard Schussek · webmozart.io 116/122 class Order { public function

    setCustomerNumber(int $orderNumber) { // ... } } What About Strict Models?
  71. Bernhard Schussek · webmozart.io 117/122 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'data_class' => PlaceOrder::class, ]); } class PlaceOrder { public $orderNumber; public $customerNumber; // ... } ✓ Map to DTOs
  72. Bernhard Schussek · webmozart.io 118/122 class PlaceOrder { /** @OrderNumber

    */ public $orderNumber; } /** @Entity */ class Order { /** @OrderNumber */ private $orderNumber; } ✓ Use Domain-Specific Constraints
  73. Bernhard Schussek · webmozart.io 119/122 public function configureOptions(OptionsResolver $resolv {

    $resolver->setDefaults([ 'data_class' => Order::class, ]); } /** * @Entity */ class Order { ... } ✓ Map to Doctrine Entities in RAD Applications
  74. Bernhard Schussek · webmozart.io 120/122 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() Twig FormBuilder Form Order FormView FormView update validate() render()
  75. Bernhard Schussek · webmozart.io 121/122 $builder ->add('country', 'entity', [ ...

    ]) ->add('province', 'entity', [ ... ]) ; What About Field Dependencies?
  76. Bernhard Schussek · webmozart.io 123/122 $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)