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

Leveraging forms and validation with Symfony

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.

Hugo Hamon

March 01, 2013
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

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

    View Slide

  2. About me…
    Hugo HAMON
    Head of training at SensioLabs
    Book author
    Speaker at conferences
    Symfony contributor
    @hhamon

    View Slide

  3. SensioLabs
    We’re hiring
    Symfony and
    PHP top stars.

    View Slide

  4. Introduction
    Forms & Validation

    View Slide

  5. Why is form handling complex?

    View Slide

  6. Forms
    Architecture

    View Slide

  7. View Slide

  8. Form engine
    Core CSRF DI
    Doctrine Propel
    Twig PHP Smarty …
    Zend …
    Rendering
    Extensions
    Foundation

    View Slide

  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.*"
    }
    }

    View Slide

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

    View Slide

  11. Forms
    The Basics

    View Slide

  12. Creating a simple form
    $form = $formFactory
    ->createBuilder()
    ->add('name')
    ->add('bio', 'textarea')
    ->add('gender', 'choice', array(
    'choices' => array(
    'm' => 'Male',
    'f' => 'Female'
    ),
    ))
    ->getForm();

    View Slide

  13. Creating a simple form

    View Slide

  14. The form tree
    form   form bio   textarea
    name   text
    gender   choice
    Field name   Field type  

    View Slide

  15. Form handling
    $name = $form->getName();
    if (!empty($_POST[$name])) {
    $form->bind($_POST[$name]);
    $data = $form->getData();
    print_r($data);
    }

    View Slide

  16. Array mapping
    $data['name']
    $data['bio']
    $data['gender']

    View Slide

  17. Prepolutation
    $form->setData(array(
    'name' => 'Jane Smith',
    'bio' => 'I do great things!',
    'gender' => 'f',
    ));

    View Slide

  18. Prepolutation
    Jane Smith
    I do great things!
    Female

    View Slide

  19. Forms
    Core field types

    View Slide

  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

    View Slide

  21. Native form types
    Text Choice Password File
    Form
    Date
    Country Language Timezone Birthday DateTime

    View Slide

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

    View Slide

  23. The repeated eld is
    rendered as two same
    password elds.

    View Slide

  24. Forms
    Rendering

    View Slide

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

    View Slide

  26. Prototyping (PHP)


    Your profile
    widget($form) ?>


    Save changes
    Cancel


    View Slide

  27. Prototyping (Twig)


    Your profile
    {{ form_widget(form) }}


    Save changes
    Cancel


    View Slide


  28. class="required">Name
    name="form[name]"
    required="required"
    value="Jane Smith" />

    Prototyping

    View Slide

  29. // Form rendering
    enctype($form) ?>
    widget($form) ?>
    errors($form) ?>
    rest($form) ?>
    // Field rendering
    row($form['bio']) ?>
    errors($form['bio']) ?>
    label($form['bio']) ?>
    widget($form['bio'], array(
    'attr' => array('class' => 'editor'),
    )) ?>
    Piece by piece rendering (PHP)

    View Slide

  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)

    View Slide

  31. Forms
    Object mapping

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. Private properties mapping
    class Person
    {
    private $active;
    // ...
    public function isActive()
    {
    return $this->active;
    }
    }

    View Slide

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

    View Slide

  37. Private properties mapping
    $form = $formFactory
    ->createBuilder('form', $person, array(
    'data_class' => 'Person',
    ))
    // ...
    ->getForm()
    ;

    View Slide

  38. Forms
    CSRF Protection

    View Slide

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

    View Slide

  40. Enabling CSRF protection


    Your profile






    Save changes
    Cancel


    View Slide

  41. Forms
    Custom form class

    View Slide

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

    View Slide

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

    View Slide

  44. Creating a custom type
    $person = new Person();
    $person->setName('John Doe');
    // ...
    $options = array('trim' => true);
    $form = $formFactory
    ->create(new PersonType(), $person, $options)
    ;

    View Slide

  45. Register new custom types
    # in your code...
    $form = $formFactory->create('person', $person);
    # bootstrap.php
    $formFactory = Forms::createFormFactoryBuilder()
    // [...]
    ->addType(new PersonType(), 'person')
    ->getFormFactory()
    ;

    View Slide

  46. Forms
    File upload

    View Slide

  47. Handling file uploads
    # bootstrap.php
    // ...
    use Symfony\…\HttpFoundation\HttpFoundationExtension;
    $formFactoryBuilder = Forms::createFormFactoryBuilder()
    $formFactory = $formFactoryBuilder
    ->addExtension(new HttpFoundationExtension())
    // [...]
    ->getFormFactory()
    ;

    View Slide

  48. Handling file uploads
    class PersonType extends AbstractType
    {
    function buildForm(FormBuilderInterface $builder, …)
    {
    $builder
    // […]
    ->add('picture', 'file', array(
    'required' => false,
    ))
    ->add('active', 'checkbox')
    ;
    }
    }

    View Slide

  49. Rendering the enctype attribute



    View Slide

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

    View Slide

  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

    View Slide

  52. Handling the form
    $file = $form->get('picture')->getData();
    $target = __DIR__. '/uploads';
    if ($file->isValid()) {
    $new = $file->move($target, 'jdoe.jpg');
    }

    View Slide

  53. Forms
    Unmapped fields

    View Slide

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

    View Slide

  55. array(
    'name' => 'John Doe'
    'password' => 'secret'
    'bio' => 'Actor!'
    'gender' => 'm'
    'picture' => null
    'active' => true
    )
    Unmapping fields
    No « rules » data

    View Slide

  56. Forms
    Collections

    View Slide

  57. Embedding a fields collection
    $builder
    // ...
    ->add('hobbies', 'collection', array(
    'allow_add' => true,
    'allow_delete' => true,
    ))
    // ...
    ;

    View Slide

  58. Embedding a fields collection
    $person = new Person();
    $person->setName('John Doe');
    $person->addHobby('music');
    $person->addHobby('movies');
    $person->addHobby('travels');

    View Slide

  59. Embedding a fields collection

    View Slide

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

    View Slide

  61. Data prototype

    View Slide

  62. Forms
    Embedded forms

    View Slide

  63. Embedding a form into another
    class Address
    {
    private $street;
    private $zipCode;
    private $city;
    private $state;
    private $country;
    // ...
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  67. Embedding a form into another
    $formBuilder = Forms::createFormFactoryBuilder();
    $formFactory = $formBuilder
    // ...
    ->addType(new AddressType(), 'address')
    ->addType(new PersonType(), 'person')
    ->getFormFactory()
    ;

    View Slide

  68. The form tree
    form   form bio   textarea
    name   text
    …   ...
    Field   Type  
    address   Address
    zipCode   text
    address   textarea
    city   text
    country   country
    state   text

    View Slide

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

    View Slide

  70. Forms
    I18N & L10N

    View Slide

  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

    View Slide

  72. Locale: fr_FR
    Locale: en_US

    View Slide

  73. Forms
    Theming

    View Slide

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

    {{ block('field_widget') }}




    {% endblock password_widget %}
    Changing all password fields

    View Slide

  75. {% form_theme form _self %}
    {% block _person_username_widget %}

    {{ block('field_widget') }}




    {% endblock _person_username_widget %}
    Changing one single field

    View Slide

  76. Custom fields rendering

    View Slide

  77. Validation
    The Validator

    View Slide

  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

    View Slide

  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.

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  83. Validating the form

    View Slide

  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…

    View Slide

  85. Validation
    Configuration formats

    View Slide

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

    View Slide

  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

    View Slide

  88. # src/config/validation.yml
    Person:
    properties:
    name:
    - NotBlank: ~
    - Length: { 'min': 5, 'max': 40 }
    # ...
    getters:
    # ...
    constraints:
    # ...
    YAML validation mapping

    View Slide








  89. 5
    40




    XML validation mapping

    View Slide

  90. use Symfony\Component\Validator\Constraints as Assert;
    class Person
    {
    /**
    * @Assert\NotBlank()
    * @Assert\Length(min = 5, max = 40)
    */
    private $name;
    // ...
    }
    Annotations validation mapping

    View Slide

  91. Validation
    Adding constraints

    View Slide

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

    View Slide

  93. class Person
    {
    //...
    static function loadValidatorMetadata(ClassMetadata $metadata)
    {
    $metadata->addConstraint(new Unique(array(
    'field' => 'username',
    'message' => 'This username already exist.',
    )));
    //...
    }
    }
    Class constraints

    View Slide

  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

    View Slide

  95. Method constraints
    The error message is
    not mapped to the
    password eld. It’s a
    global error…

    View Slide

  96. class PersonType extends AbstractType
    {
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
    $resolver->setDefaults(array(
    'data_class' => 'Person',
    'error_mapping' => array('passwordValid' => 'password'),
    ));
    }
    }
    Method constraints

    View Slide

  97. Method constraints
    The error message is
    now reaffected to the
    password concrete
    eld.

    View Slide

  98. Validation
    Translating messages

    View Slide

  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

    View Slide



  100. original="file.ext">


    Password and username cannot be same.
    Le mot de passe et le nom d'utilisateur doivent
    être différents.




    Translating messages

    View Slide

  101. Validation
    Advanced configurations

    View Slide

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

    View Slide

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

    View Slide

  104. In the end

    View Slide

  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

    View Slide

  106. Thank you!
    Hugo Hamon
    [email protected]
    @hhamon

    View Slide