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. About me… Hugo HAMON Head of training at SensioLabs Book

    author Speaker at conferences Symfony contributor @hhamon
  2. Form engine Core CSRF DI Doctrine Propel Twig PHP Smarty

    … Zend … Rendering Extensions Foundation
  3. 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.*" } }
  4. 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();
  5. Creating a simple form $form = $formFactory ->createBuilder() ->add('name') ->add('bio',

    'textarea') ->add('gender', 'choice', array( 'choices' => array( 'm' => 'Male', 'f' => 'Female' ), )) ->getForm();
  6. The form tree form   form bio   textarea name

      text gender   choice Field name   Field type  
  7. 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
  8. Native form types Text Choice Password File Form Date Country

    Language Timezone Birthday DateTime …
  9. 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() ;  
  10. 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(), ));
  11. 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>
  12. 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>
  13. // 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)
  14. {# 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)
  15. 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);
  16. Private properties mapping class Person { private $name; // ...

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

    public function isActive() { return $this->active; } }
  18. 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);
  19. 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();
  20. 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>
  21. 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'; } }
  22. // ... 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') ; } }
  23. Creating a custom type $person = new Person(); $person->setName('John Doe');

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

    $formFactory->create('person', $person); # bootstrap.php $formFactory = Forms::createFormFactoryBuilder() // [...] ->addType(new PersonType(), 'person') ->getFormFactory() ;
  25. Handling file uploads # bootstrap.php // ... use Symfony\…\HttpFoundation\HttpFoundationExtension; $formFactoryBuilder

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

    $builder, …) { $builder // […] ->add('picture', 'file', array( 'required' => false, )) ->add('active', 'checkbox') ; } }
  27. 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
  28. Handling the form $file = $form->get('picture')->getData(); $target = __DIR__. '/uploads';

    if ($file->isValid()) { $new = $file->move($target, 'jdoe.jpg'); }
  29. array( 'name' => 'John Doe' 'password' => 'secret' 'bio' =>

    'Actor!' 'gender' => 'm' 'picture' => null 'active' => true ) Unmapping fields No « rules » data
  30. Embedding a fields collection $builder // ... ->add('hobbies', 'collection', array(

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

    $person->addHobby('music'); $person->addHobby('movies'); $person->addHobby('travels');
  32. 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]); } } }
  33. Embedding a form into another class Address { private $street;

    private $zipCode; private $city; private $state; private $country; // ... }
  34. 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'; } }
  35. Embedding a form into another class PersonType extends AbstractType {

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

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

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

      text …   ... Field   Type   address   Address zipCode   text address   textarea city   text country   country state   text
  39. 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 ) )
  40. $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
  41. {% 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
  42. {% 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
  43. // ... 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
  44. 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.
  45. // ... 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
  46. $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
  47. Validating the form $request = Request::createFromGlobals(); $request->overrideGlobals(); if ($request->isMethod('POST')) {

    $form->bind($request); if ($form->isValid()) { $data = $form->getData(); // ... handle sanitized data } }
  48. 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…
  49. 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
  50. # src/config/validation.yml Person: properties: name: - NotBlank: ~ - Length:

    { 'min': 5, 'max': 40 } # ... getters: # ... constraints: # ... YAML validation mapping
  51. <!-- 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
  52. use Symfony\Component\Validator\Constraints as Assert; class Person { /** * @Assert\NotBlank()

    * @Assert\Length(min = 5, max = 40) */ private $name; // ... } Annotations validation mapping
  53. 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, ))); // ... } }
  54. class Person { //... static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addConstraint(new

    Unique(array( 'field' => 'username', 'message' => 'This username already exist.', ))); //... } } Class constraints
  55. 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
  56. Method constraints The error message is not mapped to the

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

    $resolver->setDefaults(array( 'data_class' => 'Person', 'error_mapping' => array('passwordValid' => 'password'), )); } } Method constraints
  58. 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
  59. <?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
  60. 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'), ))); // ... } }
  61. 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'), )); } }