Est-ce que je dois changer ma cartouche Brita ?

0 gravatar Par Grégoire Marchal - 04/04/2014

Ne vous êtes-vous jamais posé cette question, vous qui avez une carafe Brita ? Eh bien moi, si. Souvent même, depuis que la pile de l'afficheur de la carafe est morte !

C'est ce qui m'a donné l'idée de créer un petit site web que je viens de mettre en prod. Ce site, c'est Est-ce que je dois changer ma cartouche ? Une interface volontairement simpliste, juste les fonctionnalités nécessaires, le tout propulsé par Symfony2 évidemment !

Si ça vous intéresse, le code est disponible sur github.

Ma petite contribution à Symfony

3 gravatar Par Grégoire Marchal - 10/03/2013

Depuis sa dernière mise à jour (la version 2.2.0), Symfony embarque un petit bout de code dont je suis le vénérable auteur. Pour être exact, c'est dans le bundle "SensioFrameworkExtraBundle" qu'il se trouve. Il apporte une nouvelle option à l'objet DoctrineParamConverter qui permet, en résumé, d'optimiser le nombre de requêtes faites à la base de données lors de l'utilisation du ParamConverter.

Pour plus d'informations, je vous invite à lire la doc du ParamConverter, particulièrement l'option "repository_method". C'est moi qui l'ai fait :)
En bonus, la pull request en question sur GitHub.

Sinon, en bref, j'ai changé de boulot, je travaille depuis fin décembre pour Attkraktiv/Efidev, spécialistes de l'agilité et de la qualité, et c'est génial !
Seconde petite info, je viens de migrer le site. J'ai quitté mon VKS pour un serveur dédié kimsufi !

Une alternative aux CAPTCHAs avec Symfony2

9 gravatar Par Grégoire Marchal - 05/09/2012

Encore un peu de sécurité, un domaine important, appliqué à la lutte contre le spam via les formulaires cette fois. Je ne voulais vous infliger des CAPTCHAs sur mon blog, j'ai donc cherché une méthode alternative pour éviter les bots. Je suis rapidement tombé sur le principe du "honeypot button" via cet article. L'idée est que les bots vont globalement soit soumettre le formulaire sans "cliquer" sur un bouton, soit "cliquer" sur le premier qui ressemble à un bouton "submit". Donc on en affiche plusieurs, et on vérifie côté serveur que c'est bien le bon bouton qui a été utilisé.

Symfoniquement parlant, il faut donc créer un type de champ personnalisé pour représenter les boutons à afficher. Il faut également ajouter une validation pour vérifier que le bon bouton a été utilisé.

Création du type de champ

Tout d'abord, mes boutons doivent avoir une valeur, afin d'être identifiables lors de la phase de validation. Ces valeurs sont définies en conf, dans le fichier parameters.yml, afin d'être facilement récupérables via le service container :

# app/config/parameters.yml
parameters:
    # anti spam buttons values
    antispam_button:
        good: xuitybzerpidvfugvo
        bad:  kuserhxvvkigyhsdsf

Je leur ai donné des valeurs arbitraires, je n'étais pas inspiré !

Ensuite, je crée mon type de champ à proprement parler. Je crée donc la classe AntispamButtonType. Elle implémente ContainerAwareInterface puisque l'on va faire joujou avec le service container. Je définis le parent de mon Type à choice, puisque finalement, ce groupe de boutons sert à faire un choix, choisir le bon bouton pour prouver qu'on n'est pas un robot. Voici donc cette classe :

// src/.../BlogBundle/Form/AntispamButtonType.php

class AntispamButtonType extends AbstractType implements ContainerAwareInterface
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->setContainer($container);
    }

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $antispamParams = $this->container->getParameter('antispam_button');

        $resolver->setDefaults(array(
            'choices'  => array(
                $antispamParams['bad']  => 'do.not.add.comment',
                $antispamParams['good'] => 'add.comment'
            ),
        ));
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        parent::buildView($view, $form, $options);

        $view->vars['btn_classes'] = array('btn', 'btn btn-primary');
        $view->vars['label_prefix'] = array(null, '<i class="icon-comment icon-white"></i> ');
    }

    public function getParent()
    {
        return 'choice';
    }

    public function getName()
    {
        return 'antispambutton';
    }
}

J'utilise donc le service container pour aller récupérer les valeurs des boutons, que j'associe à leurs labels. J'ai également surchargé la méthode buildView afin de faire quelques modifications qui touchent au rendu final des boutons (classes CSS, icône dans le bouton).

Intéressons nous au rendu justement. Afin que Symfony sache comment il doit afficher ce champ, il faut définir un template twig comme ceci :

{# src/.../BlogBundle/Resources/views/Form/fields.html.twig #}

{% block antispambutton_widget %}
    {% for key, choice in choices %}
        <button name="{{ full_name }}" id="{{ id }}_{{ key }}" value="{{ choice.value }}" type="submit" class="{{ btn_classes[key] }}">
            {{ label_prefix[key] is not null ? label_prefix[key]|raw : '' }}{{ choice.label|trans }}
        </button>
    {% endfor %}
{% endblock %}

{% block antispambutton_label %}{% endblock %}

Je crée donc un bloc antispambutton_widget ("antispambutton" doit correspondre à ce qui est renvoyé par la méthode AntispamButtonType::getName()) pour le rendu du champ, et un bloc antispambutton_label vide, pour dire que je ne veux pas de libellé pour ce champ. Il faut maintenant prévenir le framework qu'il a un fichier à parser pour le rendu des formulaires. Ca se passe dans le fichier config.yml :

# app/config/config.yml
twig:
    form:
        resources:
            - 'MyCompanyBlogBundle:Form:fields.html.twig'

Notre type de champ est maintenant prêt à être utilisé ! On peut donc l'instancier dans l'un de nos formulaires. Dans mon cas, il s'agit de CommentType, pour ajouter un commentaire. Avant cela, il faut également rendre cette classe "container aware" (comme on l'avait fait pour AntispamButtonType), puisque l'on va avoir besoin du container pour instancier notre type de champ (et on en aura aussi besoin pour la validation, voir paragraphes suivants). Voici donc le résultat :

// src/.../BlogBundle/Form/CommentType.php

class CommentType extends AbstractType implements ContainerAwareInterface
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->setContainer($container);
    }

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // other fields...

            ->add('button',  new AntispamButtonType($this->container), array(
                'property_path' => false,
            ))
        ;
    }
}

N'oubliez pas l'option property_path, pour préciser que ce champ n'est pas à mapper sur l'objet Comment. Nos boutons s'affichent donc bien désormais, il ne reste plus que la validation.

Validation

Pour la validation, il ne nous reste qu'à vérifier que la valeur du bouton qui a été utilisé est la valeur du bon bouton. Pour cela, il y a juste quelques lignes à ajouter dans CommentType::buildForm(). Initialement, je voulais utiliser la classe CallbackValidator, mais celle-ci est dépréciée depuis la version 2.1. Il faut donc, comme indiqué dans la description de cette classe, utiliser le form event FormEvents::POST_BIND. Voici comment :

// src/.../BlogBundle/Form/CommentType.php

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $container = $this->container;

    $builder
        // other fields...

        // antispam button + validation
        ->add('button',  new AntispamButtonType($this->container), array(
            'property_path' => false,
        ))
        ->addEventListener(FormEvents::POST_BIND, function(FormEvent $event) use($container) {
            $form = $event->getForm();
            $antispamParams = $container->getParameter('antispam_button');

            if (!$form->has('button') || $form['button']->getData() !== $antispamParams['good']) {
                $form->addError(new FormError('spam.detected'));
            }
        })
    ;
}

Et voilà, désormais, une erreur est levée si on ne clique pas sur le bon bouton. Il ne reste qu'à traduire la chaîne spam.detected dans les fichiers validators.XX.yml. On a désormais un système antispam à la fois ergonomique et efficace !


Edit (10/09/2012)

Suite au commentaire pertinent de Jakub Lédl, j'ai sensiblement revu ma copie. Passer le service container de cette manière n'est pas la bonne manière de faire. Les types de champs ne sont pas sensés avoir à faire au container. La solution est de déclarer le nouveau type de champ en tant que service, et de lui injecter les paramètres directement.

Commençons par déclarer le service :

# src/.../BlogBundle/Resources/config/services.yml
services:
    form.type.antispam_button:
        class: GregoireM\Bundle\BlogBundle\Form\AntispamButtonType
        arguments:
            - %antispam_button%
        tags:
            - { name: form.type, alias: antispambutton }

Il faut ensuite modifier la classe AntispamButtonType :

class AntispamButtonType extends AbstractType
{
    protected $params;

    public function __construct(array $params)
    {
        $this->params = $params;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'choices'  => array(
                $this->params['bad']  => 'do.not.add.comment',
                $this->params['good'] => 'add.comment'
            ),
        ));
    }

    // ...
}

On peut alors utiliser notre type de champ juste avec son alias "antispambutton". Voici donc les changements de la classe CommentType, débarrassée de son container :

// src/.../BlogBundle/Form/CommentType.php

class CommentType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // other fields...

            // antispam button + validation
            ->add('button', 'antispambutton', array(
                'property_path' => false,
            ))
            ->addEventListener(FormEvents::POST_BIND, function(FormEvent $event) {
                $form = $event->getForm();
                $antispamParams = $form->getConfig()
                    ->getFormFactory()
                    ->getType('antispambutton')
                    ->getParams();

                if (!$form->has('button') || $form['button']->getData() !== $antispamParams['good']) {
                    $form->addError(new FormError('spam.detected'));
                }
            })
        ;
    }

  // ...
}

Notez qu'il faut également ajouter la méthode AntispamButtonType::getParams() pour récupérer le tableau de paramètre afin de l'utiliser dans la callback de validation. Je n'ai pas trouvé de meilleur moyen de faire ça...