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

Ingénierie inversée du composant Form de Symfony

Hugo Hamon
April 08, 2014

Ingénierie inversée du composant Form de Symfony

Le composant de formulaire du framework Symfony2 est un système à la fois complexe et ingénieux qui offre aux développeurs une API simplifiée pour composer des formulaires web. Cette API de haut niveau couplée au moteur de rendu Twig n'est en fait que la partie visible de l'iceberg. Ce sujet de conférence propose au public de découvrir toute la partie immergée de cet immense iceberg ! Il s'agit ici d'étudier en détail l'architecture interne du composant de formulaire pour en comprendre tous les rouages tels que la configuration, le « mapping », la normalisation des données ou bien encore le filtrage des données grâce aux événements.

Hugo Hamon

April 08, 2014
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Ingénierie inversée du
    composant Form
    Sarah Khalil
    Hugo Hamon
    https://www.flickr.com/photos/orlandolawyer/6120103619/sizes/o/in/photostream/

    View full-size slide

  2. Vos humbles serviteurs
    Sarah Khalil
    Cultivatrice de projets
    @
    Hugo Hamon
    Responsable des formations
    @
    @hhamon
    @saro0h

    View full-size slide

  3. De quoi allons-nous vous parler?

    View full-size slide

  4. Au menu
    I. Les formulaires au quotidien
    II. Construction d’un formulaire
    III.Traitement du formulaire

    View full-size slide

  5. Quelques chiffres

    View full-size slide

  6. Quelques chiffres
    45 568/311 349 LOC
    52 Dossiers
    292 Fichiers
    53 Namespaces
    30 Interfaces
    272 Classes
    38 Constantes
    2 312 Méthodes

    View full-size slide

  7. Sur Github
    • Récupérer le composant :
    • via Composer
    • https://github.com/symfony/form
    • +200 issues ouvertes

    View full-size slide

  8. Les dépendances
    Event Dispatcher
    Options Resolver
    Property Access
    Intl
    HttpKernel
    HttpFoundation
    Validator
    Dependency Injection
    Twig Bridge
    Security (CSRF)
    Obligatoires Facultatives

    View full-size slide

  9. Problématiques

    View full-size slide

  10. On nous demande ceci

    View full-size slide

  11. On le traduit comme ça

    View full-size slide

  12. Objet métier (domain object pour les intimes)
    namespace Live\ArticleBundle\Entity;
    !
    class Article
    {
    private $title;
    private $tags;
    private $publishedAt;
    !
    public function __construct($title, array $tags, $publishedAt)
    {
    $this->title = $title;
    $this->tags = $tags;
    !
    if (!$publishedAt instanceof \DateTime) {
    $publishedAt = new \DateTime($publishedAt);
    }
    !
    $this->publishedAt = $publishedAt;
    }
    }

    View full-size slide

  13. ArticleType (définition des éléments du formulaire)
    namespace Live\ArticleBundle\Form;
    !
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    !
    class ArticleType extends AbstractType
    {
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('title')
    ->add('tags')
    ->add('publishedAt', 'datetime')
    ;
    }
    !
    // ...
    }

    View full-size slide

  14. Le contrôleur (utilisation du formulaire)
    namespace Live\ArticleBundle\Controller;
    !
    // use ...
    class ArticleController extends Controller
    {
    /** @Template */
    public function newAction(Request $request)
    {
    $article = new Article('The Symfony Book', [ 'php', 'http' ], '2014-04-08 12:00');
    !
    $form = $this->createForm(new ArticleType(), $article);
    $form->handleRequest($request);
    !
    if ($form->isValid()) {
    // process the data
    }
    !
    return [ 'form' => $form->createView() ];
    }
    }

    View full-size slide

  15. Du point de vue de l’humain

    View full-size slide

  16. Du point de vue de Symfony
    article
    ArticleType
    title
    TextType
    tags
    TextType
    publishedAt
    DateTimeType
    date
    DateType
    time
    TimeType
    year
    ChoiceType
    month
    ChoiceType
    day
    ChoiceType
    hour
    ChoiceType
    minute
    ChoiceType

    View full-size slide

  17. Ce qu’on pense communément du
    Composant Form

    View full-size slide

  18. Construction d’un formulaire

    View full-size slide

  19. La notion de type
    Un type est une structure unique qui définit et configure
    un élément du formulaire

    View full-size slide

  20. $ php app/console container:debug | grep form.type
    Les types natifs sont enregistrés en tant que services avec le tag « form.type »

    View full-size slide

  21. Héritage statique : Structure des types
    AbstractType
    TextType
    EmailType
    ChoiceType
    DateTimeType

    FormTypeInterface

    View full-size slide

  22. Héritage dynamique de type
    class DateType extends AbstractType
    {
    public function getParent()
    {
    return 'form';
    }
    }
    !
    class BirthdayType extends AbstractType
    {
    public function getParent()
    {
    return 'date';
    }
    }

    View full-size slide

  23. Comment ça fonctionne ?
    http://cdn-parismatch.ladmedia.fr

    View full-size slide

  24. Revenons à notre contrôleur
    class ArticleController extends Controller
    {
    /** @Template */
    public function newAction(Request $request)
    {
    $article = new Article('...');
    !
    $form = $this->createForm(new ArticleType(), $article);
    $form->handleRequest($request);
    !
    // ...
    }
    }

    View full-size slide

  25. Dans un premier temps
    $form = $this->createForm(new ArticleType(), $article);
    $form = $this->createForm(‘article’, $article);
    ou
    $this->container->get('form.factory')->create($type, $data, $options);
    ou
    Symfony\Component\Form\FormFactory

    View full-size slide

  26. createResolvedType($type)
    new ResolvedFormType()
    Return the Form object
    createBuilder()
    buildForm($builder)
    return the FormBuilder object
    getForm()
    FormFactory ResolvedFormTypeFactory ResolvedFormType Form
    FormBuilder
    Vue d’ensemble

    View full-size slide

  27. createResolvedType($type)
    new ResolvedFormType()
    Return the Form object
    createBuilder()
    buildForm($builder)
    return the FormBuilder object
    getForm()
    FormFactory ResolvedFormTypeFactory ResolvedFormType Form
    FormBuilder
    Vue d’ensemble

    View full-size slide

  28. Résolution dynamique d’un type
    namespace Symfony\Component\Form;
    !
    class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface
    {
    public function createResolvedType(
    FormTypeInterface $type,
    array $typeExtensions,
    ResolvedFormTypeInterface $parent = null
    )
    {
    return new ResolvedFormType($type, $typeExtensions, $parent);
    }
    }

    View full-size slide

  29. createResolvedType($type)
    new ResolvedFormType()
    Return the Form object
    createBuilder()
    buildForm($builder)
    return the FormBuilder object
    getForm()
    FormFactory ResolvedFormTypeFactory ResolvedFormType Form
    FormBuilder
    Vue d’ensemble

    View full-size slide

  30. Composition d’un type résolu
    // birthday type
    $type = new ResolvedFormType(
    new BirthdayType(), [], new ResolvedFormType(
    new DateType(), [], new ResolvedFormType(
    new FormType(), []
    )
    )
    );

    View full-size slide

  31. Composition d’un type résolu
    ResolvedFormType
    BirthdayType DateType FormType
    ResolvedFormType
    ResolvedFormType

    View full-size slide

  32. Collecter les infos de chaque type
    // text type
    $collector = new FormDataCollector(…);
    !
    $type = new ResolvedTypeDataCollectorProxy(
    new ResolvedFormType(
    new TextType(),
    [],
    new ResolvedTypeDataCollectorProxy(
    new ResolvedFormType(new FormType(), []),
    $collector
    ),
    ),
    $collector
    );

    View full-size slide

  33. Débogage des formulaires

    View full-size slide

  34. createResolvedType($type)
    new ResolvedFormType()
    Return the Form object
    createBuilder()
    buildForm($builder)
    return the FormBuilder object
    getForm()
    FormFactory ResolvedFormTypeFactory ResolvedFormType Form
    FormBuilder
    Vue d’ensemble

    View full-size slide

  35. Création du FormBuilder
    !
    • Construit et configure l’instance de formulaire
    • Résout toutes les options avec l’OptionsResolver
    • Enregistre les écouteurs et les convertisseurs de données
    • 1 ResolvedFormType 1 FormBuilder 1 EventDispatcher

    View full-size slide

  36. createResolvedType($type)
    new ResolvedFormType()
    Return the Form object
    createBuilder()
    buildForm($builder)
    return the FormBuilder object
    getForm()
    FormFactory ResolvedFormTypeFactory ResolvedFormType Form
    FormBuilder
    Vue d’ensemble

    View full-size slide

  37. Préparation du FormBuilder
    namespace Live\ArticleBundle\Form;
    !
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    !
    class ArticleType extends AbstractType
    {
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('title')
    ->add('tags')
    ->add('publishedAt', 'datetime')
    ;
    }
    !
    // ...
    }

    View full-size slide

  38. createResolvedType($type)
    new ResolvedFormType()
    Return the Form object
    createBuilder()
    buildForm($builder)
    return the FormBuilder object
    getForm()
    FormFactory ResolvedFormTypeFactory ResolvedFormType Form
    FormBuilder
    Vue d’ensemble

    View full-size slide

  39. Dans la méthode FormBuilder::getForm()
    // Création du formulaire
    $form = new Form($formConfig);
    foreach ($this->getChildren() as $child) {
    $form->add($child);
    }
    !
    // Initialisation des valeurs par défaut
    $form->setData($article);

    View full-size slide

  40. Création du formulaire
    // Création du formulaire
    $form = new Form($formConfig);
    foreach ($this->getChildren() as $child) {
    $form->add($child);
    }
    !
    // Initialisation des valeurs par défaut
    $form->setData($article);

    View full-size slide

  41. Et voilà ! Le formulaire
    article
    Form
    title
    Form
    tags
    Form
    publishedAt
    Form
    date
    Form
    time
    Form
    year
    Form
    month
    Form
    day
    Form
    hour
    Form
    minute
    Form

    View full-size slide

  42. Initialisation des valeurs par défaut
    $form->setData($article);

    View full-size slide

  43. article
    new Article(…)
    title
    ‘The Symfony Book’
    tags
    ‘php, http’
    publishedAt
    DateTime(’2014-04-08 12:00’)
    date
    [ ‘year’ => 2014, … ]
    time
    [ ‘hour’ => 12, … ]
    year
    ‘2014’
    month
    ’04'
    day
    ’08'
    hour
    ’12'
    minute
    ’00'
    setData() setData()
    setData()
    setData() setData()
    setData()
    setData()
    setData()
    setData()
    setData()

    View full-size slide

  44. Composition d’un formulaire
    Form
    EventDispatcher
    FormConfigBuilder
    RequestHandler
    DataMapper
    FormFactory
    ModelTransformer[]
    ViewTransformer[]
    Options

    View full-size slide

  45. Récapitulons
    FormFactory
    ResolvedFormType
    FormBuilder
    Form
    Type résolu contenant :
    ✓ les types de formulaire assemblés
    ✓ options résolues.
    Fabrique des instances de formulaires à partir de
    types de formulaires
    Configure un seul formulaire
    Instance du formulaire chargée de traiter les
    données qui lui sont fournies
    résout
    crée
    construit

    View full-size slide

  46. Normalisation des données
    https://www.flickr.com/photos/infofestival

    View full-size slide

  47. 3 représentations des données
    Les données
    appartenant au
    modèle de
    données
    Les données
    normalisées
    permettent le
    passage d’une
    représentation à
    une autre.
    Les données
    utilisables pour
    l’affichage des
    informations
    dans le
    navigateur.
    Model Data NormData View Data

    View full-size slide

  48. 2 règles d’Or
    Ne pas changer le contenu de la donnée mais
    seulement sa représentation
    Les données normalisées doivent contenir le
    maximum d’informations.

    View full-size slide

  49. Changer la représentation
    '2014-04-08' => '2015-09-10'
    '2014-04-08' => DateTime('2014-04-08')

    View full-size slide

  50. Définition des valeurs par défaut du formulaire
    Model Data NormData View Data
    Normalisation / Transformation
    POST_SET_DATA
    PRE_SET_DATA
    FormEvent
    DataTransformers

    View full-size slide

  51. Normaliser les tags de l’article
    ['php', 'http'] ['php', 'http'] 'php, http'
    Normalisation / Transformation
    DataTransformers

    View full-size slide

  52. Les convertisseurs de données
    Les convertisseurs de données aka « DataTransformer »
    modifient la représentation des données.

    View full-size slide

  53. Créer un convertisseur de données
    namespace Live\ArticleBundle\Form\DataTransformer;
    !
    use Symfony\Component\Form\DataTransformerInterface;
    use Symfony\Component\Form\Exception\TransformationFailedException;
    !
    class ArrayToDelimitedStringTransformer implements DataTransformerInterface
    {
    public function transform($value)
    {
    if (!is_array($value)) {
    throw new TransformationFailedException();
    }
    !
    if (0 === count($value)) {
    return '';
    }
    !
    return implode(', ', $value);
    }
    }

    View full-size slide

  54. Enregistrer un convertisseur de données
    $builder->addViewTransformer(
    new ArrayToDelimitedStringTransformer()
    );
    !
    $builder->addModelTransformer(
    new My\Custom\ModelDataTransformer()
    );

    View full-size slide

  55. Quelques convertisseurs natifs
    DataTransformer
    !"" ArrayToPartsTransformer.php
    !"" BaseDateTimeTransformer.php
    !"" BooleanToStringTransformer.php
    !"" ChoiceToBooleanArrayTransformer.php
    !"" ChoiceToValueTransformer.php
    !"" ChoicesToBooleanArrayTransformer.php
    !"" ChoicesToValuesTransformer.php
    !"" DataTransformerChain.php
    !"" DateTimeToArrayTransformer.php
    !"" DateTimeToLocalizedStringTransformer.php
    !"" DateTimeToRfc3339Transformer.php
    !"" DateTimeToStringTransformer.php
    !"" DateTimeToTimestampTransformer.php
    !"" IntegerToLocalizedStringTransformer.php
    !"" MoneyToLocalizedStringTransformer.php
    !"" NumberToLocalizedStringTransformer.php
    !"" PercentToLocalizedStringTransformer.php
    #"" ValueToDuplicatesTransformer.php

    View full-size slide

  56. Soumission des données
    https://www.flickr.com/photos/safaguezguez/4782575498/sizes/l/in/photostream/

    View full-size slide

  57. Vous vous souvenez ?
    class ArticleController extends Controller
    {
    /** @Template */
    public function newAction(Request $request)
    {
    $article = new Article('...');
    !
    $form = $this->createForm(new ArticleType(), $article);
    $form->handleRequest($request);
    !
    // ...
    }
    }

    View full-size slide

  58. C’est reparti !
    class ArticleController extends Controller
    {
    /** @Template */
    public function newAction(Request $request)
    {
    $article = new Article('...');
    !
    $form = $this->createForm(new ArticleType(), $article);
    $form->handleRequest($request);
    !
    // ...
    }
    }

    View full-size slide

  59. Soumission du formulaire
    $form->submit($request->request->get(‘article’));
    ou
    $form->handleRequest($request);

    View full-size slide

  60. HttpFoundationRequestHandler
    • Vérifie que le formulaire est soumis avec le bon type de requête
    • Récupère les données depuis la requête
    • $request->query | $request->request | $request->files
    • Appelle la méthode Form::submit()

    View full-size slide

  61. Form::submit() est appelé sur tous les objets Form fils
    Réintégration des données au modèle
    MD
    ND
    VD
    Dénormalisation
    PRE_SUBMIT
    FormEvent
    DataTransformers
    SUBMIT
    ND
    POST_SUBMIT
    VD
    Dénormalisation

    View full-size slide

  62. Dénormaliser une chaîne de tags
    class ArrayToDelimitedStringTransformer implements DataTransformerInterface
    {
    public function reverseTransform($value)
    {
    if (!is_string($value)) {
    throw new TransformationFailedException();
    }
    !
    if (empty($value)) {
    return array();
    }
    !
    return explode(', ', $value);
    }
    }

    View full-size slide

  63. Les événements
    Les événements du formulaire permettent de modifier le
    contenu de la donnée ou bien de modifier la structure
    du formulaire.

    View full-size slide

  64. Utilisation des événements
    Constante Nom de l’événement Objet de l’événement
    PRE_SET_DATA form.pre_set_data FormEvent($form, $modelData)
    POST_SET_DATA form.post_set_data FormEvent($form, $modelData)
    PRE_SUBMIT form.pre_bind FormEvent($form, $submittedData)
    SUBMIT form.bind FormEvent($form, $normData)
    POST_SUBMIT form.post_bind FormEvent($form, $viewData)

    View full-size slide

  65. Quels usages ?
    !
    • Ajouter / Supprimer des champs dans une collection
    • Ajouter des champs en fonction de l’utilisateur
    • Modifier / Filter le contenu donné
    !
    • Exemple : changer la casse d’une chaîne de caractères
    • Exemple : alimenter des listes déroulantes liées

    View full-size slide

  66. Mettre le titre de l’article en « title case »
    // ArticleType.php
    use Symfony\Component\Form\FormEvent;
    use Symfony\Component\Form\FormEvents;
    !
    // Dans la méthode ArticleType::buildForm()
    $titleBuilder = $builder->create('title');
    $titleBuilder->addEventListener(
    FormEvents::PRE_SUBMIT, function (FormEvent $event) {
    $title = ucwords($event->getData());
    $event->setData($title);
    }
    );
    !
    $builder->add($titleBuilder);

    View full-size slide

  67. Enregistrer un « event subscriber »
    $builder->add(!
    $builder!
    ->create('title')!
    ->addEventSubscriber(!
    ! ! ! ! ! ! new StringUtilEventSubscriber()!
    ! ! ! ! ! )!
    );

    View full-size slide

  68. A vous de jouer !

    View full-size slide

  69. [email protected]
    https://www.flickr.com/photos/bigbrotherbackpacking

    View full-size slide

  70. Fin
    [email protected] // [email protected] // @poledev
    https://www.flickr.com/photos/nesto

    View full-size slide