Slide 1

Slide 1 text

Bernhard Schussek · webmozart.io 1/100 https://www.flickr.com/photos/pennuja/5364128968 (CC BY 2.0) Symfony Forms 101 Bernhard Schussek (@webmozart) – Symfony Catalunya 2016

Slide 2

Slide 2 text

Bernhard Schussek · webmozart.io 4/100 Bernhard Schussek Freelancer, Trainer and Coach Symfony Architecture Coding Practices webmozart.io

Slide 3

Slide 3 text

Bernhard Schussek · webmozart.io 5/100 @webmozart

Slide 4

Slide 4 text

Bernhard Schussek · webmozart.io 6/100 What's the hardest part of Symfony?

Slide 5

Slide 5 text

Bernhard Schussek · webmozart.io 7/100

Slide 6

Slide 6 text

Bernhard Schussek · webmozart.io 8/100

Slide 7

Slide 7 text

Bernhard Schussek · webmozart.io 9/100

Slide 8

Slide 8 text

Bernhard Schussek · webmozart.io 10/100

Slide 9

Slide 9 text

Bernhard Schussek · webmozart.io 11/100 Make simple cases simple, make complex cases possible ✓

Slide 10

Slide 10 text

Bernhard Schussek · webmozart.io 12/100 Forms are not as hard as you think

Slide 11

Slide 11 text

Bernhard Schussek · webmozart.io 13/100 PlaceOrderType Controller FormFactory createForm() buildForm() FormBuilder Form

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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()

Slide 14

Slide 14 text

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()

Slide 15

Slide 15 text

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()

Slide 16

Slide 16 text

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()

Slide 17

Slide 17 text

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()

Slide 18

Slide 18 text

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()

Slide 19

Slide 19 text

Bernhard Schussek · webmozart.io 21/100 My Best Practices

Slide 20

Slide 20 text

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()

Slide 21

Slide 21 text

Bernhard Schussek · webmozart.io 23/100 ✓ Use PhpStorm

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Bernhard Schussek · webmozart.io 27/100 $defaultShipping = $this->getDefaultShipping($customer); $form = $this->createForm(PlaceOrderType::class, .., [ 'default_shipping' => $defaultShipping, ]); ✓ Use Custom Options

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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()

Slide 29

Slide 29 text

Bernhard Schussek · webmozart.io 31/100 {{ form(form) }} ✓ Use form(form) for Rapid Prototyping

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Bernhard Schussek · webmozart.io 34/100 ✓ Use the Form Debugger

Slide 33

Slide 33 text

Bernhard Schussek · webmozart.io 35/100
{{ form_errors(form.paymentMethod) }} Payment Method {{ form_widget(form.paymentMethod) }}
✓ Write Custom HTML By Hand

Slide 34

Slide 34 text

Bernhard Schussek · webmozart.io 36/100 {% form_theme form 'my-theme.html.twig' %} {{ form(form) }} ✗ (Mostly) Don't Use Themes

Slide 35

Slide 35 text

Bernhard Schussek · webmozart.io 37/100

Slide 36

Slide 36 text

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); }

Slide 37

Slide 37 text

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()

Slide 38

Slide 38 text

Bernhard Schussek · webmozart.io 40/100 {% for tab in form.tabs %} {{ tab.vars.label }} {% for field in tab %} {{ form_row(field) }} {% endfor %} {% endfor %}

Slide 39

Slide 39 text

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()

Slide 40

Slide 40 text

Bernhard Schussek · webmozart.io 42/100 /** * @Assert\NotNull * @Assert\GreaterThan(0) */ private $cost; ✗ Don't Add "Assert" Alias

Slide 41

Slide 41 text

Bernhard Schussek · webmozart.io 43/100 ✓ Use PhpStorm with the Annotation Plugin

Slide 42

Slide 42 text

Bernhard Schussek · webmozart.io 44/100 /** * @NotNull * @GreaterThan(0) */ private $cost; ✓ Auto-Import Annotations

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Bernhard Schussek · webmozart.io 47/100 $context->addViolation('product.total.invalid'); ✓ Use Message Keys

Slide 45

Slide 45 text

Bernhard Schussek · webmozart.io 48/100 $context->buildViolation('product.total.invalid') ->setParameter('{{ expected }}', $expected) ->addViolation(); ✓ Use Message Parameters

Slide 46

Slide 46 text

Bernhard Schussek · webmozart.io 49/100 /** * @NotNull * @GreaterThan(0, groups="Checkout") */ private $total; ✓ Use Validation Groups for Partial Validation

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Bernhard Schussek · webmozart.io 53/100 class ProductNameValidator extends ConstraintValidator { public function validated($value, Constraint $constr { if (null === $value) { return; } // ... } } ✓ Ignore Null Values

Slide 51

Slide 51 text

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()

Slide 52

Slide 52 text

Bernhard Schussek · webmozart.io 55/100 Clark Kent ... Submit Name VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3

Slide 53

Slide 53 text

Bernhard Schussek · webmozart.io 56/100 $builder->add('vat', TextType::class, [ 'property_path' => 'customer.vatId' ]); ✓ Decouple Forms from the Model Structure

Slide 54

Slide 54 text

Bernhard Schussek · webmozart.io 57/100 'customer.name' == $data->getCustomer()->setName() 'items[0].amount' == $data->getItems()[0]->setAmount() ✓ Learn Property Paths

Slide 55

Slide 55 text

Bernhard Schussek · webmozart.io 58/100 class Customer { public function __construct($name, Address $address) { // ... } } What About Non-Empty Constructors?

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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?

Slide 58

Slide 58 text

Bernhard Schussek · webmozart.io 61/100 Model Format View Format ProductRef string ProductRefType

Slide 59

Slide 59 text

Bernhard Schussek · webmozart.io 62/100 Model Format View Format Money string MyMoneyType

Slide 60

Slide 60 text

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)

Slide 61

Slide 61 text

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)

Slide 62

Slide 62 text

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)

Slide 63

Slide 63 text

Bernhard Schussek · webmozart.io 66/100 class Customer { public function relocate(Address $address) { // ... } } What About Non-Standard Methods?

Slide 64

Slide 64 text

Bernhard Schussek · webmozart.io 67/100 Clark Kent ... Submit Name VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3

Slide 65

Slide 65 text

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)

Slide 66

Slide 66 text

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)

Slide 67

Slide 67 text

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)

Slide 68

Slide 68 text

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()

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Bernhard Schussek · webmozart.io 77/100 $builder->add('expressShipping', ChoiceType::class, [ // ... // $shipping->getTitle() 'choice_label' => 'title', ]); Decide later No Yes

Slide 71

Slide 71 text

Bernhard Schussek · webmozart.io 78/100 $builder->add('expressShipping', ChoiceType::class, [ // ... 'choice_label' => function (Selection $selection) { return humanize($selection->getType()); }, ]); Decide later No Yes

Slide 72

Slide 72 text

Bernhard Schussek · webmozart.io 79/100 $builder->add('expressShipping', ChoiceType::class, [ // ... // $selection->getType() 'choice_value' => 'type' ]); Decide later No Yes

Slide 73

Slide 73 text

Bernhard Schussek · webmozart.io 80/100 $builder->add('paymentMethod', ChoiceType::class, [ // ... // $paymentMethod->getCategory()->getName() 'group_by' => 'category.name', ]); ... ...

Slide 74

Slide 74 text

Bernhard Schussek · webmozart.io 81/100 $builder ->add('country', 'entity', [ ... ]) ->add('province', 'entity', [ ... ]) ; What About Field Dependencies?

Slide 75

Slide 75 text

Bernhard Schussek · webmozart.io 82/100 $builder->get('country')->addEventListener( FormEvents::POST_SET_DATA, $addProvinceField ); $builder->get('country')->addEventListener( FormEvents::POST_SUBMIT, $addProvinceField ); ✓ Use POST_* Hooks On Fields (1/2)

Slide 76

Slide 76 text

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)

Slide 77

Slide 77 text

Bernhard Schussek · webmozart.io 84/100 Forms and CRUD

Slide 78

Slide 78 text

Bernhard Schussek · webmozart.io 85/100 class Contact { private $firstName; private $lastName; private $email; private $phoneNumber; private $address; // accessor methods... } Example: Contact Details

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

Bernhard Schussek · webmozart.io 87/100 class Contact { // ... public function setAddress(Address $address) { $this->address = $address; } } ✓ Write Strict Method Signatures

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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()); } // ... }

Slide 84

Slide 84 text

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 } } }

Slide 85

Slide 85 text

Bernhard Schussek · webmozart.io 92/100 KISS

Slide 86

Slide 86 text

Bernhard Schussek · webmozart.io 93/100 No Home for Business Logic

Slide 87

Slide 87 text

Bernhard Schussek · webmozart.io 94/100 Forms and DDD/CQRS

Slide 88

Slide 88 text

Bernhard Schussek · webmozart.io 95/100 class Contact { ... } Aggregate Root class ContactCommandHandler { ... } Command Handler class ModifyContact { ... } Command

Slide 89

Slide 89 text

Bernhard Schussek · webmozart.io 96/100 $commandBus->dispatch( new ModifyContact($fieldValues) ); ✓ Dispatch Commands in the Controller

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

Bernhard Schussek · webmozart.io 99/100 What about Forms?

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

Bernhard Schussek · webmozart.io 102/100 Lots of Boilerplate

Slide 96

Slide 96 text

Bernhard Schussek · webmozart.io 103/100 Well-Structured Business Logic

Slide 97

Slide 97 text

Bernhard Schussek · webmozart.io 104/100 CRUD or DDD or CQRS?

Slide 98

Slide 98 text

Bernhard Schussek · webmozart.io 105/100 Forms can be simple if done right!

Slide 99

Slide 99 text

Bernhard Schussek · webmozart.io 106/100 Complex things remain complex...

Slide 100

Slide 100 text

Bernhard Schussek · webmozart.io 107/100 Questions? Questions? Bernhard Schussek Bernhard Schussek @webmozart @webmozart webmozart.io