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

Intégrer les formulaires et la validation Symfo...

Hugo Hamon
April 05, 2013

Intégrer les formulaires et la validation Symfony dans vos applications PHP.

Les formulaires sont partout et sont aussi des composants importants d'une application. Ils permettent en effet aux utilisateurs d'interagir avec votre système d'informations. Cependant, la conception et la validation des formulaires ne sont pas des tâches si aisées qu'elles n'y paraissent. Le but de cette conférence est de vous aider à tirer profit des composants de formulaire et de validation du framework Symfony. Nous aborderons les aspects d'architecture des formulaires, les concepts fondamentaux ainsi que quelques usages avancés comme les formulaires imbriqués et les collections. Nous passerons aussi en revue les différentes manières de valider des données avec le composant de validation. Vous serez ainsi en mesure d'intégrer ces deux composants majeurs du framework Symfony dans vos propres applications PHP.

Hugo Hamon

April 05, 2013
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Intégrer les formulaires et la validation Symfony dans vos applications

    PHP . Hugo HAMON – SymfonyLive - Paris 2013
  2. Cœur du système de formulaire Core CSRF DI Doctrine Propel

    Twig PHP Smarty … Zend … Rendu Extensions Fondation
  3. 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.*" } }
  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. Créer un formulaire simple $form = $formFactory ->createBuilder() ->add('name') ->add('bio',

    'textarea') ->add('gender', 'choice', array( 'choices' => array( 'm' => 'Male', 'f' => 'Female' ), )) ->getForm();
  6. Vue arborescente du formulaire form   form bio   textarea

    name   text gender   choice Nom du champ   Type de champ  
  7. 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
  8. Types de champ natifs Text Choice Password File Form Date

    Country Language Timezone Birthday DateTime …
  9. 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() ;  
  10. Le type « répété » est rendu sous la forme

    de deux champs de saisie de mot de passe.
  11. 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(), ));
  12. Prototypage du rendu (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>
  13. Prototypage du rendu (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>
  14. // 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'), )) ?> Rendu personnalisé (PHP)
  15. {# 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)
  16. 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);
  17. Association des propriétés privées class Person { private $name; //

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

    ... public function isActive() { return $this->active; } }
  19. 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);
  20. Association du type de données $form = $formFactory ->createBuilder('form', $person,

    array( 'data_class' => 'Person', )) // ... ->getForm() ;
  21. 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();
  22. Activer la protection CSRF <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>
  23. 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'; } }
  24. // ... 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') ; } }
  25. Créer un type personnalisé $person = new Person(); $person->setName('John Doe');

    // ... $options = array('trim' => true); $form = $formFactory ->create(new PersonType(), $person, $options) ;
  26. Enregistrer un nouveau type # in your code... $form =

    $formFactory->create('person', $person); # bootstrap.php $formFactory = Forms::createFormFactoryBuilder() // [...] ->addType(new PersonType(), 'person') ->getFormFactory() ;
  27. Gérer l’envoi de fichiers # bootstrap.php // ... use Symfony\…\HttpFoundation\HttpFoundationExtension;

    $formFactoryBuilder = Forms::createFormFactoryBuilder() $formFactory = $formFactoryBuilder ->addExtension(new HttpFoundationExtension()) // [...] ->getFormFactory() ;
  28. Gérer l’envoi de fichiers class PersonType extends AbstractType { function

    buildForm(FormBuilderInterface $builder, …) { $builder // […] ->add('picture', 'file', array( 'required' => false, )) ->add('active', 'checkbox') ; } }
  29. 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
  30. Traiter le formulaire $file = $form->get('picture')->getData(); $target = __DIR__. '/uploads';

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

    'Actor!' 'gender' => 'm' 'picture' => null 'active' => true ) Champ non associé Pas de donnée « rules »
  32. Définir une collection de champs $builder // ... ->add('hobbies', 'collection',

    array( 'allow_add' => true, 'allow_delete' => true, )) // ... ;
  33. Définir une collection de champs $person = new Person(); $person->setName('John

    Doe'); $person->addHobby('music'); $person->addHobby('movies'); $person->addHobby('travels');
  34. 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]); } } }
  35. Imbriquer les formulaires class Address { private $street; private $zipCode;

    private $city; private $state; private $country; // ... }
  36. 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'; } }
  37. Imbriquer les formulaires class PersonType extends AbstractType { function buildForm(FormBuilderInterface

    $builder, …) { $builder // ... ->add('address', new AddressType()) ; } }
  38. Imbriquer les formulaires $formBuilder = Forms::createFormFactoryBuilder(); $formFactory = $formBuilder //

    ... ->addType(new AddressType(), 'address') ->addType(new PersonType(), 'person') ->getFormFactory() ;
  39. Vue arborescente du formulaire form   form bio   textarea

    name   text …   ... Champ   Type   address   Address zipCode   text address   textarea city   text country   country state   text
  40. 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 ) )
  41. $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
  42. {% 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 %} Changer le rendu de tous les champs de saisie de mot de passe
  43. {% 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 %} Changer le rendu d’un champ précis
  44. // ... 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
  45. 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.
  46. // ... 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
  47. $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
  48. Validation du formulaire $request = Request::createFromGlobals(); $request->overrideGlobals(); if ($request->isMethod('POST')) {

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

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

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

    Unique(array( 'field' => 'username', 'message' => 'This username already exist.', ))); //... } } Contraintes de classe
  56. 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
  57. 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
  58. Associer une erreur globale à un champ Le message d’erreur

    est désormais réaffecté au champ « password ».
  59. 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
  60. <?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> Traduire les messages d’erreur
  61. 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'), ))); // ... } }
  62. 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'), )); } }