Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

SensioLabs We’re hiring Symfony and PHP top stars.

Slide 4

Slide 4 text

Introduction Forms & Validation

Slide 5

Slide 5 text

Why is form handling complex?

Slide 6

Slide 6 text

Forms Architecture

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Forms The Basics

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Creating a simple form

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Prepolutation Jane Smith I do great things! Female

Slide 19

Slide 19 text

Forms Core field types

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

The repeated eld is rendered as two same password elds.

Slide 24

Slide 24 text

Forms Rendering

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Prototyping (PHP) Your profile widget($form) ?>
Save changes Cancel

Slide 27

Slide 27 text

Prototyping (Twig) Your profile {{ form_widget(form) }}
Save changes Cancel

Slide 28

Slide 28 text

Name
Prototyping

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Forms Object mapping

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Forms CSRF Protection

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Enabling CSRF protection Your profile
Save changes Cancel

Slide 41

Slide 41 text

Forms Custom form class

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Forms File upload

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Rendering the enctype attribute

Slide 50

Slide 50 text

Handling the form use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals() $request->overrideGlobals(); if ($request->isMethod('post')) { $form->bind($request); var_dump($form->getData()); }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Forms Unmapped fields

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Forms Collections

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Embedding a fields collection

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Data prototype

Slide 62

Slide 62 text

Forms Embedded forms

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Forms I18N & L10N

Slide 71

Slide 71 text

$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

Slide 72

Slide 72 text

Locale: fr_FR Locale: en_US

Slide 73

Slide 73 text

Forms Theming

Slide 74

Slide 74 text

{% form_theme form _self %} {% block password_widget %}
{{ block('field_widget') }}
{% endblock password_widget %} Changing all password fields

Slide 75

Slide 75 text

{% form_theme form _self %} {% block _person_username_widget %}
{{ block('field_widget') }}
{% endblock _person_username_widget %} Changing one single field

Slide 76

Slide 76 text

Custom fields rendering

Slide 77

Slide 77 text

Validation The Validator

Slide 78

Slide 78 text

// ... 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

Slide 79

Slide 79 text

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.

Slide 80

Slide 80 text

// ... 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

Slide 81

Slide 81 text

$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

Slide 82

Slide 82 text

Validating the form $request = Request::createFromGlobals(); $request->overrideGlobals(); if ($request->isMethod('POST')) { $form->bind($request); if ($form->isValid()) { $data = $form->getData(); // ... handle sanitized data } }

Slide 83

Slide 83 text

Validating the form

Slide 84

Slide 84 text

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…

Slide 85

Slide 85 text

Validation Configuration formats

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

5 40 XML validation mapping

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Validation Adding constraints

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

Validation Translating messages

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Validation Advanced configurations

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

In the end

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

Thank you! Hugo Hamon hugo.hamon@sensiolabs.com @hhamon