Share
Embed
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
Transcript
100 slides
-
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 -
Bernhard Schussek · webmozart.io 4/100 Bernhard Schussek Freelancer, Trainer and
Coach Symfony Architecture Coding Practices webmozart.io -
Bernhard Schussek · webmozart.io 11/100 Make simple cases simple, make
complex cases possible ✓ -
Bernhard Schussek · webmozart.io 14/100 PlaceOrderType FormFactory Controller createForm() buildForm()
Form handleRequest() Order Validator createView() FormBuilder Form Order FormView update validate() -
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() -
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() -
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() -
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() -
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() -
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() -
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() -
Bernhard Schussek · webmozart.io 24/100 $form = $this->createForm(PaymentType::class, $order, [
'action' => $this->generateUrl('payment_gw'), ]); ✓ Use "action" option in the controller -
Bernhard Schussek · webmozart.io 25/100 $form = $this->createForm(PaymentType::class, $order, [
'method' => 'PUT', ]); ✓ Use "method" option in the controller -
Bernhard Schussek · webmozart.io 26/100 $defaultShipping = $this->getDefaultShipping($customer); $form =
$this->createForm( new PlaceOrderType($defaultShipping), ); ✗ Don't Pass Dynamic Data to Constructor -
Bernhard Schussek · webmozart.io 27/100 $defaultShipping = $this->getDefaultShipping($customer); $form =
$this->createForm(PlaceOrderType::class, .., [ 'default_shipping' => $defaultShipping, ]); ✓ Use Custom Options -
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 -
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 -
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() -
Bernhard Schussek · webmozart.io 31/100 {{ form(form) }} ✓ Use
form(form) for Rapid Prototyping -
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 -
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 -
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 -
Bernhard Schussek · webmozart.io 36/100 {% form_theme form 'my-theme.html.twig' %}
{{ form(form) }} ✗ (Mostly) Don't Use Themes -
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); } -
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() -
Bernhard Schussek · webmozart.io 40/100 {% for tab in form.tabs
%} {{ tab.vars.label }} {% for field in tab %} {{ form_row(field) }} {% endfor %} {% endfor %} -
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() -
Bernhard Schussek · webmozart.io 42/100 /** * @Assert\NotNull * @Assert\GreaterThan(0)
*/ private $cost; ✗ Don't Add "Assert" Alias -
Bernhard Schussek · webmozart.io 43/100 ✓ Use PhpStorm with the
Annotation Plugin -
Bernhard Schussek · webmozart.io 44/100 /** * @NotNull * @GreaterThan(0)
*/ private $cost; ✓ Auto-Import Annotations -
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 -
Bernhard Schussek · webmozart.io 48/100 $context->buildViolation('product.total.invalid') ->setParameter('{{ expected }}', $expected)
->addViolation(); ✓ Use Message Parameters -
Bernhard Schussek · webmozart.io 49/100 /** * @NotNull * @GreaterThan(0,
groups="Checkout") */ private $total; ✓ Use Validation Groups for Partial Validation -
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 -
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 -
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 -
Bernhard Schussek · webmozart.io 53/100 class ProductNameValidator extends ConstraintValidator {
public function validated($value, Constraint $constr { if (null === $value) { return; } // ... } } ✓ Ignore Null Values -
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() -
Bernhard Schussek · webmozart.io 55/100 Clark Kent ... Submit Name
VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3 -
Bernhard Schussek · webmozart.io 56/100 $builder->add('vat', TextType::class, [ 'property_path' =>
'customer.vatId' ]); ✓ Decouple Forms from the Model Structure -
Bernhard Schussek · webmozart.io 57/100 'customer.name' == $data->getCustomer()->setName() 'items[0].amount' ==
$data->getItems()[0]->setAmount() ✓ Learn Property Paths -
Bernhard Schussek · webmozart.io 58/100 class Customer { public function
__construct($name, Address $address) { // ... } } What About Non-Empty Constructors? -
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 -
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? -
Bernhard Schussek · webmozart.io 61/100 Model Format View Format ProductRef
string ProductRefType -
Bernhard Schussek · webmozart.io 62/100 Model Format View Format Money
string MyMoneyType -
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) -
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) -
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) -
Bernhard Schussek · webmozart.io 66/100 class Customer { public function
relocate(Address $address) { // ... } } What About Non-Standard Methods? -
Bernhard Schussek · webmozart.io 67/100 Clark Kent ... Submit Name
VAT $order->getCustomerName() $order->setCustomerName() Order +orderNumber +customerName +vat 1 2 3 -
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) -
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) -
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) -
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() -
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 -
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> -
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> -
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> -
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> -
Bernhard Schussek · webmozart.io 81/100 $builder ->add('country', 'entity', [ ...
]) ->add('province', 'entity', [ ... ]) ; What About Field Dependencies? -
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) -
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) -
Bernhard Schussek · webmozart.io 85/100 class Contact { private $firstName;
private $lastName; private $email; private $phoneNumber; private $address; // accessor methods... } Example: Contact Details -
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 -
Bernhard Schussek · webmozart.io 87/100 class Contact { // ...
public function setAddress(Address $address) { $this->address = $address; } } ✓ Write Strict Method Signatures -
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 -
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 -
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()); } // ... } -
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 } } } -
Bernhard Schussek · webmozart.io 95/100 class Contact { ... }
Aggregate Root class ContactCommandHandler { ... } Command Handler class ModifyContact { ... } Command -
Bernhard Schussek · webmozart.io 96/100 $commandBus->dispatch( new ModifyContact($fieldValues) ); ✓
Dispatch Commands in the Controller -
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 -
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 -
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 -
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 -
Bernhard Schussek · webmozart.io 107/100 Questions? Questions? Bernhard Schussek Bernhard
Schussek @webmozart @webmozart webmozart.io