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

Formulaires Symfony2 - Cas pratiques

Formulaires Symfony2 - Cas pratiques

Alexandre Salomé

May 08, 2013
Tweet

More Decks by Alexandre Salomé

Other Decks in Programming

Transcript

  1. 2/51 Plan • Pré-requis – Avoir lu la documentation des

    formulaires • 4 cas pratiques – Un formulaire de login – Changer de mot de passe (ancien/nouveau) – Traductions avec Doctrine – Masquer les champs pour certains utilisateurs • Explications à chaque cas
  2. 3/51 Cas n°1 – Formulaire de login « Le formulaire

    de connexion n'utilise pas le composant Form, ce qui nous oblige à dupliquer le HTML. Il faut utiliser le composant Form »
  3. 4/51 Requis par la couche de sécurité • POST /login-check

    _username _password _csrf_token (intention : authenticate) $this->get('form.csrf_provider')->generateCsrfToken('authenticate') _remember_me
  4. 5/51 Login – Le formulaire class LoginType extends AbstractType {

    public function buildForm($builder, $options) { $builder ->add('_username', 'text') ->add('_password', 'password') ->add('_remember_me', 'checkbox') ->add('submit', 'submit'); // new! } public function setDefaultOptions($resolver) { $resolver->setDefaults(array( 'csrf_field_name' => '_csrf_token', 'intention' => 'authenticate', )); } public function getName() { return 'login'; } }
  5. 8/51 Formulaires dans les contrôleurs $this->createForm($type, $data, $options) // équivalent

    à $this ->get('form.factory') ->create($type, $data, $options) ;
  6. 9/51 FormFactory $this ->get('form.factory') ->create('login') ; login[_username] login[_password] login[_remember_me] login[_csrf_token]

    $this ->get('form.factory') ->createNamed('foo', 'login') ; foo[_username] foo[_password] foo[_remember_me] foo[_csrf_token] $this ->get('form.factory') ->createNamed('', 'login') ; _username _password_ _remember_me _csrf_token
  7. 10/51 Login – Le contrôleur (1/2) public function loginAction() {

    $form = $this->get('form.factory') ->createNamed('', 'login', array( 'action' => $this->generateUrl('session_loginCheck') )); if ($error = $this->getErrorMessage()) { $form->addError(new FormError($error)); } return $this->render('...', array( 'form' => $form->createView() )); }
  8. 11/51 Login – Le contrôleur (2/2) protected function getErrorMessage() {

    $request = $this->getRequest(); $attrs = $request->attributes; $session = $request->getSession(); if ($attrs->has(SecurityContext::AUTHENTICATION_ERROR)) { $error = $attrs->get(SecurityContext::AUTHENTICATION_ERROR); } else { $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); $session->remove(SecurityContext::AUTHENTICATION_ERROR); } return $error instanceof \Exception ? $error->getMessage() : $error ; }
  9. 13/51 Cas n°1 - Conclusion • Construction d'un formulaire •

    Paramétrage du CSRF (nom de champ + intention) • Flexibilité grâce à la FormFactory • Réutilisation des templates
  10. 14/51 Cas n°2 – Changement de MDP « Je veux

    que l'utilisateur saisisse son ancien mot de passe pour en mettre un nouveau »
  11. 15/51 Cycle de vie du formulaire Construction FormBuilder Utilisation Form

    Soumission $form->bind Listeners Modifiable Lecture seulement Lecture seulement Attributs & Options Modifiable Lecture seulement Lecture seulement (Data|View) Transformers Modifiable Lecture seulement Lecture seulement Enfants / Parents Modifiable Modifiable Lecture seulement
  12. 16/51 ProfileType class ProfileType extends AbstractType { public function buildForm(FormBuilderInterface

    $builder, array $options) { $builder ->add('fullname', 'text') ->add('initials', 'text') ->add('change_password', 'change_password', array( 'virtual' => true )) ->add('submit', 'submit') ; } public function getName() { return 'profile'; } }
  13. 17/51 ChangePasswordType class ChangePasswordType extends AbstractType { // ... public

    function buildForm(FormBuilderInterface $builder, array $options) { $encoderFactory = $this->encoderFactory; $builder ->add('old_password', 'password', array( 'mapped' => false )) ->add('new_password', 'repeated', array( 'mapped' => false, 'type' => 'password' ))
  14. 18/51 change_password • Data = user • Injection du service

    d'encodage • À la soumission du formulaire – Vérifie le mot de passe – Ajoute un message d'erreur si le MDP est incorrect – Enregistre le nouveau mot de passe
  15. 19/51 Le formulaire class ChangePasswordType extends AbstractType { // ...

    public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($encoderFactory) { ... });
  16. 20/51 Le formulaire function (FormEvent $event) use ($encoderFactory) { $form

    = $event->getForm(); $user = $form->getData(); $encoder = $encoderFactory->getEncoder($user); $oldPassword = $form->get('old_password')->getData(); $newPassword = $form->get('new_password')->getData(); if (!$oldPassword || !$newPassword) { return; } if (!$user->isPasswordValid($oldPassword, $encoder)) { $form->addError(new FormError('Bad credentials')); return; } $user->setPassword($newPassword, $encoder); }
  17. 21/51 Déclaration du form type <service id="form.type.change_password" class="..."> <argument type="service"

    id="security.encoder_factory" /> <tag name="form.type" alias="change_password" /> </service>
  18. 22/51 Cas n°2 - Conclusion • Le cycle de vie

    d'un formulaire • Le rôle des FormType • Les listeners pour interagir après la construction • virtual = true – Partage la donnée avec le sous-formulaire • mapped = false – Permet de « hooker » dans un FormType
  19. 23/51 Cas n°3 – Traductions & Doctrine « Je veux

    gérer mes traductions en Javascript de manière homogène »
  20. 24/51 Le contrat • Contrôleur intact • Modèle de données

    explicite (oneToMany) • Mise en commun au niveau des formulaires
  21. 25/51 Modèle Doctrine (1/2) Acme\Entity\Product: type: entity id: ~ fields:

    upc: {type: string, length: 64, nullable: true } price: {type: price, nullable: true } oneToMany: translations: targetEntity: ProductTranslation mappedBy: product indexBy: culture cascade: [ persist, remove ]
  22. 26/51 Modèle Doctrine (2/2) Acme\Entity\ProductTranslation: type: entity id: ~ fields:

    culture: {type: string, length: 8 } title: {type: string, length: 255, nullable: true } baseline: {type: text, nullable: true } manyToOne: product: targetEntity: Product inversedBy: translations joinColumn: name: product_id referencedColumnName: id
  23. 27/51 Le contrat – le contrôleur public function editAction(Request $request,

    $id) { $product = ...; $form = $this->createForm('product', $product); if ($request->isMethod('POST') && $form->bind($request)->isValid() ) { // save and flush } return $this->render(...); }
  24. 28/51 Le contrat – l'API commune $product->getUpc(); $product->setUpc($upc); $product->getTranslations(); $trans

    = $product->getTranslation('fr_FR'); $trans->getTitle(); $trans->setTitle('Product title'); $trans->getBaseline(); $trans->setBaseline('Baseline of the product'); $product->removeTranslation($trans); $product->addTranslation(new ProductTranslation(...));
  25. 29/51 Le formulaire class TranslationsType extends AbstractType { public function

    setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'allow_add' => true, 'allow_delete' => true, 'prototype' => true, 'by_reference' => false )); } public function getParent() { return 'collection'; } public function getName() { return 'translations'; } }
  26. 30/51 Le formulaire class TranslationsType extends AbstractType { public function

    __construct($defaultCulture, array $availableCultures = array()) { $this->availableCultures = $availableCultures; $this->defaultCulture = $defaultCulture; } public function buildView(FormView $view, FormInterface $form, array $options) { $cultures = $this->availableCultures; $existing = array_keys($form->all()); $view->vars['missing_cultures'] = array_diff($cultures, $existing); $view->vars['default_culture'] = $this->defaultCulture; } }
  27. 31/51 Templating E-mail : alexandre@... Prénom : Salomé Nom :

    Alexandre Cet e-mail est déjà utilisé Il est interdit d'utiliser le domaine « @... »
  28. 32/51 Templating E-mail : alexandre@... Prénom : Salomé Nom :

    Alexandre Cet e-mail est déjà utilisé Il est interdit d'utiliser le domaine « @... » LABEL WIDGET LABEL WIDGET LABEL WIDGET ERRORS
  29. 33/51 Templating E-mail : alexandre@... Prénom : Salomé Nom :

    Alexandre Cet e-mail est déjà utilisé Il est interdit d'utiliser le domaine « @... » LABEL WIDGET LABEL WIDGET LABEL WIDGET ERRORS ROW
  30. 34/51 Templating E-mail : alexandre@... Prénom : Salomé Nom :

    Alexandre Cet e-mail est déjà utilisé Il est interdit d'utiliser le domaine « @... » ROW ROW ROW
  31. 35/51 form_div_layout.html.twig {% block ..._widget %} {% block ..._label %}

    {% block ..._errors %} {% block ..._row %} {% block birthday_row %} {% block date_row %} {% block form_row %} Au moment du rendu, le moteur cherche un bloc dans le template correspondant au type ou au parent le plus proche. Par exemple pour le type « birthday » : Ce fichier comporte les blocs utilisés pour le rendu des formulaires. Les plus fréquents sont les suivants :
  32. 36/51 La vue {% block translations_row %} {% spaceless %}

    ... {% endspaceless %} {% endblock %}
  33. 37/51 {% block translations_row %} {% spaceless %} <div class="translations-container"

    data-id="{{ id }}" {% if prototype is defined %}data-prototype="{{ form_widget(prototype)|e }}"{% endif %}> <ul class="nav nav-tabs nav-translations"> {% for key, subForm in form.children %} <li{{ key == default_culture ? ' class="active"' : '' }}> <a data-toggle="tab" href="#{{ id }}-{{ key }}">{{ key }}</a> </li> {% endfor %} {% if prototype is defined %} {% for culture in missing_cultures %} <li><a data-toggle="tab" href="#{{ id }}-{{ culture }}" data-translation-create="{{ culture }}"><i class="icon-plus"></i> {{ culture }}</a></li> {% endfor %} {% endif %} </ul> <div class="form-translations"> {% for key, subForm in form.children %} <div class="tab-pane{{ loop.first ? ' active' : '' }}" id="{{ id }}-{{ key }}" {{ form_widget(subForm) }} </div> {% endfor %} </div> </div> <hr /> {% endspaceless %} {% endblock %} La vue
  34. 38/51 Le Javascript $(document).on('click', '.nav-translations a[data-translation-create]', function (e) { e.preventDefault();

    var $link = $(e.currentTarget); var $container = $($link.parents(".translations-container")[0]); var $translations = $container.find('.form-translations'); var culture = $link.attr('data-translation-create'); var prototype = $container.attr('data-prototype'); var id = $container.attr('data-id'); prototype = prototype.replace(/__name__/g, culture) ; $link.find('i.icon-plus').remove(); $translations.find(".tab-pane").removeClass('active'); var newCulture = $translations.append( '<div class="tab-pane active" id="' + id + '-' + culture + '">' + prototype + '</div>' ); $link.removeAttr('data-translation-create'); });
  35. 40/51 Implémentation class ProductType extends AbstractType { public function buildForm(...)

    { $builder ->add('upc', 'text') ->add('active', 'checkbox') ->add('translations', 'translations', array( 'type' => 'product_translation' )) ; } }
  36. 41/51 Conclusion • Mise en commun des templates de formulaires

    • Étendre un type de formulaire • Relation entre formulaire et modèle
  37. 42/51 Cas n°4 – Champs cachés « Je veux masquer

    certains champs pour certains utilisateurs »
  38. 43/51 Extension de type • Réutilisation horizontale de comportements •

    Exemples – CSRF • FormTypeCsrfExtension – Validation • FormTypeValidatorExtension
  39. 46/51 SecurityTypeExtension (1/3) class SecurityTypeExtension extends AbstractTypeExtension { public function

    __construct(SecurityContextInterface $context) { $this->context = $context; } /** * {@inheritDoc} */ public function getExtendedType() { return 'form'; } }
  40. 47/51 SecurityTypeExtension (2/3) // … public function buildForm(FormBuilderInterface $builder, array

    $options) { $g = $options['is_granted']; if (null === $g || $this->context->isGranted($g)) { return; } $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { $form = $event->getForm(); if ($form->isRoot()) // ... $form->getParent()->remove($form->getName()); }); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array('is_granted' => null)); }
  41. 49/51 Utilisation class ProductType extends AbstractType { public function buildForm(FormBuilderInterface

    $builder, array $options) { $builder ->add('upc', 'text', array('label' => 'Code UPC')) ->add('translations', 'translations', array( 'type' => 'product_translation', 'is_granted' => 'ROLE_MODERATOR' )) ; } }
  42. 50/51 Cas n°4 - Conclusion • Implémentation simple et utilisation

    rapide • Utiliser les options pour configurer – is_granted / is_granted_subject – data_as_subject