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

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.

Bernhard Schussek

July 22, 2016
Tweet

More Decks by Bernhard Schussek

Other Decks in Programming

Transcript

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

    Coach Symfony Architecture Coding Practices webmozart.io
  2. Bernhard Schussek · webmozart.io 14/100 PlaceOrderType FormFactory Controller createForm() buildForm()

    Form handleRequest() Order Validator createView() FormBuilder Form Order FormView update validate()
  3. 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()
  4. 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()
  5. 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()
  6. 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()
  7. 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()
  8. 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()
  9. 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()
  10. Bernhard Schussek · webmozart.io 24/100 $form = $this->createForm(PaymentType::class, $order, [

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

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

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

    $this->createForm(PlaceOrderType::class, .., [ 'default_shipping' => $defaultShipping, ]); ✓ Use Custom Options
  14. 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
  15. 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
  16. 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()
  17. 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
  18. 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
  19. 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
  20. 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); }
  21. 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()
  22. Bernhard Schussek · webmozart.io 40/100 {% for tab in form.tabs

    %} {{ tab.vars.label }} {% for field in tab %} {{ form_row(field) }} {% endfor %} {% endfor %}
  23. 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()
  24. 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
  25. Bernhard Schussek · webmozart.io 49/100 /** * @NotNull * @GreaterThan(0,

    groups="Checkout") */ private $total; ✓ Use Validation Groups for Partial Validation
  26. 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
  27. 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
  28. 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
  29. Bernhard Schussek · webmozart.io 53/100 class ProductNameValidator extends ConstraintValidator {

    public function validated($value, Constraint $constr { if (null === $value) { return; } // ... } } ✓ Ignore Null Values
  30. 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()
  31. Bernhard Schussek · webmozart.io 55/100 Clark Kent ... Submit Name

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

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

    __construct($name, Address $address) { // ... } } What About Non-Empty Constructors?
  34. 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
  35. 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?
  36. 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)
  37. 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)
  38. 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)
  39. Bernhard Schussek · webmozart.io 66/100 class Customer { public function

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

    VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3
  41. 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)
  42. 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)
  43. 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)
  44. 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()
  45. 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
  46. 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>
  47. 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>
  48. 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>
  49. 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>
  50. Bernhard Schussek · webmozart.io 81/100 $builder ->add('country', 'entity', [ ...

    ]) ->add('province', 'entity', [ ... ]) ; What About Field Dependencies?
  51. 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)
  52. Bernhard Schussek · webmozart.io 85/100 class Contact { private $firstName;

    private $lastName; private $email; private $phoneNumber; private $address; // accessor methods... } Example: Contact Details
  53. 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
  54. Bernhard Schussek · webmozart.io 87/100 class Contact { // ...

    public function setAddress(Address $address) { $this->address = $address; } } ✓ Write Strict Method Signatures
  55. 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
  56. 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
  57. 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()); } // ... }
  58. 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 } } }
  59. Bernhard Schussek · webmozart.io 95/100 class Contact { ... }

    Aggregate Root class ContactCommandHandler { ... } Command Handler class ModifyContact { ... } Command
  60. 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
  61. 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
  62. 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
  63. 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