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

Leveraging forms and validation with Symfony

E2ed7c278c8c49bb3e7fe0b7de039997?s=47 Hugo Hamon
March 01, 2013

Leveraging forms and validation with Symfony

Forms are everywhere and they are key elements for interacting with applications users. Unfortunately, designing forms is a very tedious task. Indeed, forms may contain complex behaviors and business logic to validate data. This talk will explain how to bring the Symfony forms and validation components into PHP applications to ease forms management.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

March 01, 2013
Tweet

Transcript

  1. Leveraging forms and validation with Symfony. Hugo HAMON – Confoo

    - Montreal 2013
  2. About me… Hugo HAMON Head of training at SensioLabs Book

    author Speaker at conferences Symfony contributor @hhamon
  3. SensioLabs We’re hiring Symfony and PHP top stars.

  4. Introduction Forms & Validation

  5. Why is form handling complex?

  6. Forms Architecture

  7. None
  8. Form engine Core CSRF DI Doctrine Propel Twig PHP Smarty

    … Zend … Rendering Extensions Foundation
  9. Bootstrapping { "require": { "doctrine/common": "2.*", "symfony/form": "2.2.*", "symfony/yaml": "2.2.*",

    "symfony/http-foundation": "2.2.*", "symfony/validator": "2.2.*", "symfony/config": "2.2.*", "symfony/translation": "2.2.*", "symfony/twig-bridge": "2.2.*" } }
  10. use Symfony\Bridge\Twig\Form\TwigRendererEngine; use Symfony\Component\Form\Forms; use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Form\TwigRenderer; // Define

    some constants to the main resources define('VENDOR_DIR', realpath(__DIR__ . '/../vendor')); define('DEFAULT_FORM_THEME', 'form_div_layout.html.twig'); define('VENDOR_TWIG_BRIDGE_DIR', VENDOR_DIR . '/symfony/twig-bridge/Symfony/Bridge/Twig'); define('VIEWS_DIR', realpath(__DIR__ . '/../views')); // Initialize a Twig compatible rendering engine $twig = new Twig_Environment(new Twig_Loader_Filesystem(array( VIEWS_DIR, VENDOR_TWIG_BRIDGE_DIR . '/Resources/views/Form', ))); $formEngine = new TwigRendererEngine(array(DEFAULT_FORM_THEME)); $formEngine->setEnvironment($twig); // Register the Twig Form extension $twig->addExtension(new FormExtension(new TwigRenderer($formEngine))); // Set up the Form component $formFactoryBuilder = Forms::createFormFactoryBuilder(); $formFactory = $formFactoryBuilder->getFormFactory();
  11. Forms The Basics

  12. Creating a simple form $form = $formFactory ->createBuilder() ->add('name') ->add('bio',

    'textarea') ->add('gender', 'choice', array( 'choices' => array( 'm' => 'Male', 'f' => 'Female' ), )) ->getForm();
  13. Creating a simple form

  14. The form tree form   form bio   textarea name

      text gender   choice Field name   Field type  
  15. Form handling $name = $form->getName(); if (!empty($_POST[$name])) { $form->bind($_POST[$name]); $data

    = $form->getData(); print_r($data); }
  16. Array mapping $data['name'] $data['bio'] $data['gender']

  17. Prepolutation $form->setData(array( 'name' => 'Jane Smith', 'bio' => 'I do

    great things!', 'gender' => 'f', ));
  18. Prepolutation Jane Smith I do great things! Female

  19. Forms Core field types

  20. Core field types §  Birthday §  Checkbox §  Choice § 

    Collection* §  Country §  Date §  DateTime §  File §  Hidden §  Integer §  Language §  Locale §  Money §  Number §  Password §  Percent §  Radio §  Repeated* §  Search §  Textarea §  Text §  Time §  Timezone §  Url
  21. Native form types Text Choice Password File Form Date Country

    Language Timezone Birthday DateTime …
  22. The repeated type $builder ->add('name') ->add('password', 'repeated', array( 'type' =>

    'password', 'invalid_message' => 'Passwords are not same.', 'first_options' => array('label' => 'Password'), 'second_options' => array('label' => 'Confirmation'), )) ->add('bio', 'textarea') // [...] ->getForm() ;  
  23. The repeated eld is rendered as two same password elds.

  24. Forms Rendering

  25. Rendering a form // PHP rendering echo $engine->render('profile.php', array( 'form'

    => $form->createView(), )); // Twig rendering echo $twig->render('profile.twig', array( 'form' => $form->createView(), ));
  26. Prototyping (PHP) <form action="#" method="post"> <fieldset> <legend>Your profile</legend> <?php echo

    $view['form']->widget($form) ?> </fieldset> <div class="form-actions"> <button type="submit">Save changes</button> <button type="button">Cancel</button> </div> </form>
  27. Prototyping (Twig) <form action="#" method="post"> <fieldset> <legend>Your profile</legend> {{ form_widget(form)

    }} </fieldset> <div class="form-actions"> <button type="submit">Save changes</button> <button type="button">Cancel</button> </div> </form>
  28. <div> <label for="form_name" class="required">Name</label> <input type="text" id="form_name" name="form[name]" required="required" value="Jane

    Smith" /> </div> Prototyping
  29. // Form rendering <?php echo $view['form']->enctype($form) ?> <?php echo $view['form']->widget($form)

    ?> <?php echo $view['form']->errors($form) ?> <?php echo $view['form']->rest($form) ?> // Field rendering <?php echo $view['form']->row($form['bio']) ?> <?php echo $view['form']->errors($form['bio']) ?> <?php echo $view['form']->label($form['bio']) ?> <?php echo $view['form']->widget($form['bio'], array( 'attr' => array('class' => 'editor'), )) ?> Piece by piece rendering (PHP)
  30. {# General rendering #} {{ form_enctype(form) }} {{ form_widget(form) }}

    {{ form_errors(form) }} {{ form_rest(form) }} {# Field rendering #} {{ form_row(form.bio) }} {{ form_errors(form.bio) }} {{ form_label(form.bio, 'Biography') }} {{ form_widget(form.bio, { 'attr': { 'class': 'editor' }}) }} Piece by piece rendering (Twig)
  31. Forms Object mapping

  32. Public properties mapping class Person { public $name; public $password;

    public $bio; public $gender; }
  33. Public properties mapping $person = new Person(); $person->name = 'John

    Doe'; $person->password = 'S3cR3T$1337'; $person->bio = 'Born in 1970...'; $person->gender = 'm'; $person->active = true; // The form reads & writes // the person object $form->setData($person);
  34. Private properties mapping class Person { private $name; // ...

    function setName($name) { $this->name = $name; } function getName() { return $this->name; } }
  35. Private properties mapping class Person { private $active; // ...

    public function isActive() { return $this->active; } }
  36. Private properties mapping $person = new Person(); $person->setName('John Doe'); $person->setPassword('S3cR3T$1337');

    $person->setBio('Born in 1970...'); $person->setGender('m'); $person->setActive(true); // The form reads & writes // the person object $form->setData($person);
  37. Private properties mapping $form = $formFactory ->createBuilder('form', $person, array( 'data_class'

    => 'Person', )) // ... ->getForm() ;
  38. Forms CSRF Protection

  39. Enabling CSRF protection # bootstrap.php // ... use Symfony\Component\Form\Extension\Csrf\CsrfExtension; use

    Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider; use Symfony\Bridge\Twig\Form\TwigRenderer; define('CSRF_SECRET', 'c2ioeEU1n48QF2WsHGWd2HmiuUUT6dxr'); // Set up the CSRF provider $csrfProvider = new DefaultCsrfProvider(CSRF_SECRET); $renderer = new TwigRenderer($formEngine, $csrfProvider); // ... $formFactory = $formFactoryBuilder ->addExtension(new CsrfExtension($csrfProvider)) ->getFormFactory();
  40. Enabling CSRF protection <form method="post" action="#"> <fieldset> <legend>Your profile</legend> <div

    id="form"> <!-- ... fields ... --> <input type="hidden" value="0219d[…]61d69" name="form[_token]"> </div> </fieldset> <div class="form-actions"> <button type="submit">Save changes</button> <button type="button">Cancel</button> </div> </form>
  41. Forms Custom form class

  42. Creating a custom type use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class PersonType

    extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array('data_class' => 'Person')); } public function getName() { return 'person'; } }
  43. // ... use Symfony\Component\Form\FormBuilderInterface; class PersonType extends AbstractType { public

    function buildForm( FormBuilderInterface $builder, array $options ) { $builder ->add('name') ->add('password', 'repeated', array(...)) ->add('bio', 'textarea') ->add('gender', 'choice', array(...)) ->add('active', 'checkbox') ; } }
  44. Creating a custom type $person = new Person(); $person->setName('John Doe');

    // ... $options = array('trim' => true); $form = $formFactory ->create(new PersonType(), $person, $options) ;
  45. Register new custom types # in your code... $form =

    $formFactory->create('person', $person); # bootstrap.php $formFactory = Forms::createFormFactoryBuilder() // [...] ->addType(new PersonType(), 'person') ->getFormFactory() ;
  46. Forms File upload

  47. Handling file uploads # bootstrap.php // ... use Symfony\…\HttpFoundation\HttpFoundationExtension; $formFactoryBuilder

    = Forms::createFormFactoryBuilder() $formFactory = $formFactoryBuilder ->addExtension(new HttpFoundationExtension()) // [...] ->getFormFactory() ;
  48. Handling file uploads class PersonType extends AbstractType { function buildForm(FormBuilderInterface

    $builder, …) { $builder // […] ->add('picture', 'file', array( 'required' => false, )) ->add('active', 'checkbox') ; } }
  49. Rendering the enctype attribute <form {{ form_enctype(form) }} …> <!--

    render fields --> </form>
  50. Handling the form use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals() $request->overrideGlobals(); if

    ($request->isMethod('post')) { $form->bind($request); var_dump($form->getData()); }
  51. Handling the form object(Person) private 'name' => 'John Doe' private

    'picture' => object(Symfony\Component\HttpFoundation\File\UploadedFile) private 'test' => false private 'originalName' => '445.jpg' private 'mimeType' => 'image/jpeg' private 'size' => 21645 private 'error' => 0 private 'password' => 'secret' private 'bio' => 'Famous actor!' private 'gender' => 'm' private 'active' => true
  52. Handling the form $file = $form->get('picture')->getData(); $target = __DIR__. '/uploads';

    if ($file->isValid()) { $new = $file->move($target, 'jdoe.jpg'); }
  53. Forms Unmapped fields

  54. Unmapping fields $builder ->add('rules', 'checkbox', array( 'mapped' => false, ))

    ;
  55. array( 'name' => 'John Doe' 'password' => 'secret' 'bio' =>

    'Actor!' 'gender' => 'm' 'picture' => null 'active' => true ) Unmapping fields No « rules » data
  56. Forms Collections

  57. Embedding a fields collection $builder // ... ->add('hobbies', 'collection', array(

    'allow_add' => true, 'allow_delete' => true, )) // ... ;
  58. Embedding a fields collection $person = new Person(); $person->setName('John Doe');

    $person->addHobby('music'); $person->addHobby('movies'); $person->addHobby('travels');
  59. Embedding a fields collection

  60. Coding conventions class Person { public function addHobby($hobby) { $this->hobbies[]

    = $hobby; } public function removeHobby($hobby) { $key = array_search($hobby, $this->hobbies); if (false !== $key) { unset($this->hobbies[$key]); } } }
  61. Data prototype

  62. Forms Embedded forms

  63. Embedding a form into another class Address { private $street;

    private $zipCode; private $city; private $state; private $country; // ... }
  64. class AddressType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array

    $options) { $builder ->add('street', 'textarea') ->add('zipCode') ->add('city') ->add('state') ->add('country', 'country') ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array('data_class' => 'Address')); } public function getName() { return 'address'; } }
  65. Embedding a form into another class PersonType extends AbstractType {

    function buildForm(FormBuilderInterface $builder, …) { $builder // ... ->add('address', new AddressType()) ; } }
  66. Embedding a form into another class PersonType extends AbstractType {

    function buildForm(FormBuilderInterface $builder, …) { $builder // ... ->add('address', 'address') ; } }
  67. Embedding a form into another $formBuilder = Forms::createFormFactoryBuilder(); $formFactory =

    $formBuilder // ... ->addType(new AddressType(), 'address') ->addType(new PersonType(), 'person') ->getFormFactory() ;
  68. The form tree form   form bio   textarea name

      text …   ... Field   Type   address   Address zipCode   text address   textarea city   text country   country state   text
  69. Person Object ( [name] => Hugo Hamon [picture] => [username]

    => hhamon [password] => secret [address] => Address Object ( [street] => 42 Sunshine Street [zipCode] => 12345 [city] => Miami [state] => Florida [country] => US ) [bio] => Speaker at conference [gender] => m [active] => 1 [hobbies] => Array ( [1] => movies [2] => travels [3] => conferences ) )
  70. Forms I18N & L10N

  71. $builder ->add('birthdate', 'birthday', array('format' => 'd/M/y')) ->add('salary', 'money', array('currency' =>

    'EUR')) ->add('language', 'language', array( 'preferred_choices' => array('fr'), ) ->add('country', 'country', array( 'preferred_choices' => array('FR'), ) ->add('timezone', 'timezone', array( 'preferred_choices' => array('Europe/Paris') ) ; Localized fields
  72. Locale: fr_FR Locale: en_US

  73. Forms Theming

  74. {% form_theme form _self %} {% block password_widget %} <div

    class="input-append"> {{ block('field_widget') }} <span class="add-on"> <i class="icon-lock"></i> </span> </div> {% endblock password_widget %} Changing all password fields
  75. {% form_theme form _self %} {% block _person_username_widget %} <div

    class="input-append"> {{ block('field_widget') }} <span class="add-on"> <i class="icon-user"></i> </span> </div> {% endblock _person_username_widget %} Changing one single field
  76. Custom fields rendering

  77. Validation The Validator

  78. // ... use Symfony\Component\Validator\Validation; use Symfony\Component\Form\Extension\Validator\ValidatorExtension; $validator = Validation::createValidatorBuilder() ->getValidator()

    ; $formFactoryBuilder = Forms::createFormFactoryBuilder() $formFactory = $formFactoryBuilder // ... ->addExtension(new ValidatorExtension($validator)) ->getFormFactory() ; Configuring the validator
  79. Constraint & Validator For each validation rule, the component ships

    a Constraint class and its associated Validator class. The Constraint object describes the rule to check and the Validator implementation runs that validation logic.
  80. // ... use Symfony\Component\Validator\Constraints\NotBlank as Assert; class PersonType extends AbstractType

    { public function buildForm(...) { $builder->add('name', 'text', array( 'constraints' => array( new Assert\NotBlank(), new Assert\Length(array('min' => 5, 'max' => 40)), ), )); // ... } } Field constraints mapping
  81. $genders = array('m' => 'Male', 'f' => 'Female'); $builder->add('gender', 'choice',

    array( 'choices' => $genders, 'constraints' => array( new Assert\NotBlank(), new Assert\Choice(array( 'choices' => array_keys($genders), 'message' => 'Alien genders are not supported.', )), ), )); Field constraints mapping
  82. Validating the form $request = Request::createFromGlobals(); $request->overrideGlobals(); if ($request->isMethod('POST')) {

    $form->bind($request); if ($form->isValid()) { $data = $form->getData(); // ... handle sanitized data } }
  83. Validating the form

  84. Core constraints §  All §  Blank §  Callback §  Choice

    §  Collection §  Count §  Country §  Date §  DateTime §  Email §  False §  File §  Image §  Ip §  Language §  Length §  Locale §  NotBlank §  NotNull §  Null §  Range §  Regex §  Time §  True §  Type §  Url §  Valid…
  85. Validation Configuration formats

  86. # bootstrap.php // ... $validator = Validation::createValidatorBuilder() ->addMethodMapping('loadValidatorMetadata') ->addXmlMapping(__DIR__.'/config/validation.xml') ->addYamlMapping(__DIR__.'/config/validation.yml')

    ->enableAnnotationMapping() ->getValidator() ; Configuring the validator
  87. use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Length; class Person { private

    $name; // ... public static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('name', new NotBlank()); $metadata->addPropertyConstraint('name', new Length(array( 'min' => 5, 'max' => 40, ))); // ... } } PHP validation mapping
  88. # src/config/validation.yml Person: properties: name: - NotBlank: ~ - Length:

    { 'min': 5, 'max': 40 } # ... getters: # ... constraints: # ... YAML validation mapping
  89. <!-- src/config/validation.xml --> <?xml version="1.0" ?> <constraint-mapping ...> <class name="Person">

    <property name="name"> <constraint name="NotBlank"/> <constraint name="Length"> <option name="min">5</option> <option name="max">40</option> </constraint> </property> </class> </constraint-mapping> XML validation mapping
  90. use Symfony\Component\Validator\Constraints as Assert; class Person { /** * @Assert\NotBlank()

    * @Assert\Length(min = 5, max = 40) */ private $name; // ... } Annotations validation mapping
  91. Validation Adding constraints

  92. Property constraints use Symfony\Component\Validator\Constraints\Image; class Person { private $picture; //

    ... static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('picture', new Image(array( 'minWidth' => 100, 'maxWidth' => 150, 'minHeight' => 100, 'maxHeight' => 150, ))); // ... } }
  93. class Person { //... static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addConstraint(new

    Unique(array( 'field' => 'username', 'message' => 'This username already exist.', ))); //... } } Class constraints
  94. use Symfony\Component\Validator\Constraints\True; class Person { private $username; private $password; public

    static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addGetterConstraint('passwordValid', new True(array( 'message' => 'Password and username cannot be same.', ))); // ... } public function isPasswordValid() { return strtolower($this->username) !== strtolower($this->password); } } Method constraints
  95. Method constraints The error message is not mapped to the

    password eld. It’s a global error…
  96. class PersonType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) {

    $resolver->setDefaults(array( 'data_class' => 'Person', 'error_mapping' => array('passwordValid' => 'password'), )); } } Method constraints
  97. Method constraints The error message is now reaffected to the

    password concrete eld.
  98. Validation Translating messages

  99. use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Bridge\Twig\Extension\TranslationExtension; $t = new Translator('fr');

    $t->addLoader('xlf', new XliffFileLoader()); // Built in translations $t->addResource('xlf', FORM_DIR . '/Resources/translations/validators.fr.xlf', 'fr', 'validators'); $t->addResource('xlf', VALIDATOR_DIR . '/Resources/translations/validators.fr.xlf', 'fr', 'validators'); // Your translations $t->addResource('xlf', __DIR__. '/translations/validators.fr.xlf', 'fr', 'validators'); // ... $twig->addExtension(new TranslationExtension($t)); // ... Enabling translations
  100. <?xml version="1.0"?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="en" datatype="plaintext" original="file.ext"> <body>

    <trans-unit id="1"> <source>Password and username cannot be same.</source> <target>Le mot de passe et le nom d'utilisateur doivent être différents.</target> </trans-unit> </body> </file> </xliff> Translating messages
  101. Validation Advanced configurations

  102. Validation groups class Person { public static function loadValidatorMetadata(ClassMetadata $metadata)

    { $metadata->addPropertyConstraint('password', new NotBlank(array( 'groups' => array('Signup'), ))); $metadata->addGetterConstraint('passwordValid', new True(array( 'message' => 'Password and username cannot be same.', 'groups' => array('Signup', 'Profile'), ))); // ... } }
  103. class RegistrationType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) {

    $resolver->setDefaults(array( // ... 'validation_groups' => array('Signup'), )); } } class EditAccountType extends AbstractType { public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( // ... 'validation_groups' => array('Profile'), )); } }
  104. In the end

  105. Official resources §  http://symfony.com/doc/current/book/forms.html §  http://symfony.com/doc/current/reference/forms/types.html §  http://symfony.com/doc/current/book/validation.html §  http://symfony.com/doc/current/reference/constraints.html

    §  http://symfony.com/doc/current/reference/forms/twig_reference.html §  https://github.com/bschussek/standalone-forms Useful resources
  106. Thank you! Hugo Hamon hugo.hamon@sensiolabs.com @hhamon