Slide 1

Slide 1 text

Intégrer les formulaires et la validation Symfony dans vos applications PHP . Hugo HAMON – SymfonyLive - Paris 2013

Slide 2

Slide 2 text

Biographie… Hugo HAMON Responsable des formations SensioLabs Auteur chez Eyrolles Conférencier Contributeur à Symfony @hhamon

Slide 3

Slide 3 text

SensioLabs A la recherche d’un job ?

Slide 4

Slide 4 text

Introduction Formulaires & Validation

Slide 5

Slide 5 text

Pourquoi la gestion des formulaires est-elle si compliquée ?

Slide 6

Slide 6 text

Formulaires Architecture

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Cœur du système de formulaire Core CSRF DI Doctrine Propel Twig PHP Smarty … Zend … Rendu Extensions Fondation

Slide 9

Slide 9 text

Démarrage { "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

Formulaires Les fondamentaux

Slide 12

Slide 12 text

Créer un formulaire simple $form = $formFactory ->createBuilder() ->add('name') ->add('bio', 'textarea') ->add('gender', 'choice', array( 'choices' => array( 'm' => 'Male', 'f' => 'Female' ), )) ->getForm();

Slide 13

Slide 13 text

Créer un formulaire simple

Slide 14

Slide 14 text

Vue arborescente du formulaire form   form bio   textarea name   text gender   choice Nom du champ   Type de champ  

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Association avec un tableau $data['name'] $data['bio'] $data['gender']

Slide 17

Slide 17 text

Préremplissage du formulaire $form->setData(array( 'name' => 'Jane Smith', 'bio' => 'I do great things!', 'gender' => 'f', ));

Slide 18

Slide 18 text

Préremplissage du formulaire Jane Smith I do great things! Female

Slide 19

Slide 19 text

Formulaires Types de champ natifs

Slide 20

Slide 20 text

Types de champ natifs §  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

Types de champ natifs Text Choice Password File Form Date Country Language Timezone Birthday DateTime …

Slide 22

Slide 22 text

Le champ répété $builder ->add('name') ->add('password', 'repeated', array( 'type' => 'password', 'invalid_message' => 'Passwords do not match.', 'first_options' => array('label' => 'Password'), 'second_options' => array('label' => 'Confirmation'), )) ->add('bio', 'textarea') // [...] ->getForm() ;  

Slide 23

Slide 23 text

Le type « répété » est rendu sous la forme de deux champs de saisie de mot de passe.

Slide 24

Slide 24 text

Formulaires Rendu des formulaires

Slide 25

Slide 25 text

Afficher un formulaire // 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

Prototypage du rendu (PHP) Your profile widget($form) ?>
Save changes Cancel

Slide 27

Slide 27 text

Prototypage du rendu (Twig) Your profile {{ form_widget(form) }}
Save changes Cancel

Slide 28

Slide 28 text

Name
Prototypage du rendu

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'), )) ?> Rendu personnalisé (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' }}) }} Rendu personnalisé (Twig)

Slide 31

Slide 31 text

Formulaires Association avec des objets

Slide 32

Slide 32 text

Association des propriétés publiques class Person { public $name; public $password; public $bio; public $gender; }

Slide 33

Slide 33 text

Association des propriétés publiques $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

Association des propriétés privées class Person { private $name; // ... function setName($name) { $this->name = $name; } function getName() { return $this->name; } }

Slide 35

Slide 35 text

Association des propriétés privées class Person { private $active; // ... public function isActive() { return $this->active; } }

Slide 36

Slide 36 text

Association des propriétés privées $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

Association du type de données $form = $formFactory ->createBuilder('form', $person, array( 'data_class' => 'Person', )) // ... ->getForm() ;

Slide 38

Slide 38 text

Formulaires Protection CSRF

Slide 39

Slide 39 text

Activer la protection CSRF # 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

Activer la protection CSRF Your profile
Save changes Cancel

Slide 41

Slide 41 text

Formulaires Type de formulaire personnalisé

Slide 42

Slide 42 text

Créer un type personnalisé 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

Créer un type personnalisé $person = new Person(); $person->setName('John Doe'); // ... $options = array('trim' => true); $form = $formFactory ->create(new PersonType(), $person, $options) ;

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Formulaires Gestion des fichiers

Slide 47

Slide 47 text

Gérer l’envoi de fichiers # bootstrap.php // ... use Symfony\…\HttpFoundation\HttpFoundationExtension; $formFactoryBuilder = Forms::createFormFactoryBuilder() $formFactory = $formFactoryBuilder ->addExtension(new HttpFoundationExtension()) // [...] ->getFormFactory() ;

Slide 48

Slide 48 text

Gérer l’envoi de fichiers class PersonType extends AbstractType { function buildForm(FormBuilderInterface $builder, …) { $builder // […] ->add('picture', 'file', array( 'required' => false, )) ->add('active', 'checkbox') ; } }

Slide 49

Slide 49 text

Générer l’attribut « enctype »

Slide 50

Slide 50 text

Traiter le formulaire 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

Traiter le formulaire 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

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

Slide 53

Slide 53 text

Formulaires Champs non associés

Slide 54

Slide 54 text

Définir un champ non associé $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 ) Champ non associé Pas de donnée « rules »

Slide 56

Slide 56 text

Formulaires Collections de champs

Slide 57

Slide 57 text

Définir une collection de champs $builder // ... ->add('hobbies', 'collection', array( 'allow_add' => true, 'allow_delete' => true, )) // ... ;

Slide 58

Slide 58 text

Définir une collection de champs $person = new Person(); $person->setName('John Doe'); $person->addHobby('music'); $person->addHobby('movies'); $person->addHobby('travels');

Slide 59

Slide 59 text

Définir une collection de champs

Slide 60

Slide 60 text

Conventions de nommage 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

Prototype HTML d’ajout de champ

Slide 62

Slide 62 text

Formulaires Formulaires imbriqués

Slide 63

Slide 63 text

Imbriquer les formulaires 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

Imbriquer les formulaires class PersonType extends AbstractType { function buildForm(FormBuilderInterface $builder, …) { $builder // ... ->add('address', new AddressType()) ; } }

Slide 66

Slide 66 text

Imbriquer les formulaires class PersonType extends AbstractType { function buildForm(FormBuilderInterface $builder, …) { $builder // ... ->add('address', 'address') ; } }

Slide 67

Slide 67 text

Imbriquer les formulaires $formBuilder = Forms::createFormFactoryBuilder(); $formFactory = $formBuilder // ... ->addType(new AddressType(), 'address') ->addType(new PersonType(), 'person') ->getFormFactory() ;

Slide 68

Slide 68 text

Vue arborescente du formulaire form   form bio   textarea name   text …   ... Champ   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

Formulaires 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') ) ; Types de champs régionalisés

Slide 72

Slide 72 text

Locale: fr_FR Locale: en_US

Slide 73

Slide 73 text

Formulaires Gestion des thèmes

Slide 74

Slide 74 text

{% form_theme form _self %} {% block password_widget %}
{{ block('field_widget') }}
{% endblock password_widget %} Changer le rendu de tous les champs de saisie de mot de passe

Slide 75

Slide 75 text

{% form_theme form _self %} {% block _person_username_widget %}
{{ block('field_widget') }}
{% endblock _person_username_widget %} Changer le rendu d’un champ précis

Slide 76

Slide 76 text

Rendu personnalisé de champs

Slide 77

Slide 77 text

Validation Le validateur

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() ; Configurer le validateur

Slide 79

Slide 79 text

Contraintes & Validateurs Pour chaque règle de validation, le composant embarque une classe de type Constraint et sa classe Validator associée. L’objet Constraint décrit la règle à valider et l’objet Validator est l’implementation de la logique métier associée à cette règle.

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)), ), )); // ... } } Association des contraintes aux champs

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.', )), ), )); Association des contraintes aux champs

Slide 82

Slide 82 text

Validation du formulaire $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

Validation du formulaire

Slide 84

Slide 84 text

Classes de contraintes natives §  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 Formats de configuration

Slide 86

Slide 86 text

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

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

5 40 Configuration en XML

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Validation Ajouter des contraintes

Slide 92

Slide 92 text

Contraintes de propriétés 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.', ))); //... } } Contraintes de classe

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); } } Contraintes de méthodes

Slide 95

Slide 95 text

Contraintes de méthodes Le message d’erreur remonte comme une erreur globale du formulaire.

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'), )); } } Associer une erreur globale à un champ

Slide 97

Slide 97 text

Associer une erreur globale à un champ Le message d’erreur est désormais réaffecté au champ « password ».

Slide 98

Slide 98 text

Validation Traduction des 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)); // ... Activer le support des traductions

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Validation Configuration avancée

Slide 102

Slide 102 text

Groupes de validation 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

Conclusion

Slide 105

Slide 105 text

Ressources officielles §  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 Ressources utiles

Slide 106

Slide 106 text

Merci ! Hugo Hamon [email protected] @hhamon