Gérer des utilisateurs sans FosUserBundle 5ème partie

Nous voici arrivés à la dernière partie de ce tutoriel qui nous aura permis de voir pas mal de choses: des listeners, des formsEvents, des validateurs personnalités, la mise en place de handler, le découplage du code…

Le reset de mot de passe

Nous en étions restés au fait que nous envoyions un mail à l’utilisateur après avoir vérifié qu’il existait bien en base. Mais à présent, il faut mettre en place ce contrôleur, son handler, formType & son entité, et le template. C’est parti!

 L’entité ResetPassword

Comme d’habitude, nous démarrons par l’entité:

<?php

namespace AppBundle\User\Password;

use Symfony\Component\Validator\Constraints as Assert;

class ResetPassword
{
    /**
     * @Assert\NotBlank()
     * @Assert\Length(min=8)
     */
    private $password;
    /**
     * @param string $password
     */
    public function setPassword($password)
    {
        $this->password = $password;
    }

    /**
     * @return string
     */
    public function getPassword()
    {
        return $this->password;
    }
}

Très classique, elle ne se compose que d’un champ qui sera un nouveau password à écrire deux fois.

Le ResetPasswordType

<?php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\User\Manager\UserManagerInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\User\Password\ResetPassword;

class ResetPasswordType extends AbstractType
{
    /**
     *
     * @var UserManagerInterface $handler
     */
    private $handler;

    /**
     *
     * @var Request $request
     */
    private $request;

    /**
     * @param UserManagerInterface $userManager
     */
    public function __construct(UserManagerInterface $userManager, Request $request)
    {
        $this->handler = $userManager;
        $this->request = $request;
    }
    
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('password', 'repeated', array(
            'first_name'  => 'password',
            'second_name' => 'confirm',
            'type'        => 'password',
        ));
        $builder->add('Reset Password', 'submit');

        $builder->addEventListener(
        FormEvents::PRE_SET_DATA,
            function (FormEvent $event) {
                $data = $event->getData();
                if (!$data instanceof ResetPassword) {
                    throw new \RuntimeException('ChangePassword instance required.');
                }
                $token = $this->request->query->get('token');

                if (!$token) {
                   throw new \Exception('Incorrect Token.');
                }

                $user = $this->handler->getUserByConfirmationToken($token);

                if (!$user) {
                   throw new \Exception('User not identified in our database with this token.');
                }
            }
        );
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\User\Password\ResetPassword',
        ]);
    }

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

Alors, ici, je prends des précautions. En effet, je dois vérifier que ce token est présent, mais aussi qu’il est valide en regardant du côté du repository si un user correspond bien à ce confirmationToken. Et cela, avant même de soumettre le formulaire (d’où l’event PRE_SET_DATA), quand j’arrive directement sur la page après avoir cliqué sur le lien du mail. Je lance donc diverses exceptions que j’attraperai au niveau du contrôleur.

Idem que d’habitude, ce formType ayant des dépendances, il devient un service:

<parameter key="form.type.reset_password.class">AppBundle\Form\Type\ResetPasswordType</parameter>

<service id="form.type.reset_password"
                 class="%form.type.reset_password.class%" scope="request" >
            <argument type="service" id="app.user.manager" />
            <argument type="service" id="request" />
            <tag name="form.type" alias="reset_password_form" />
</service>

Ici, j’ai rajouté scope= »request » car lorsque nous passons une request en dépendance dans Symfony2, il nous lance une erreur dont vous trouverez l’explication ici: http://symfony.com/fr/doc/current/cookbook/service_container/scopes.html#configurer-le-champ-d-application-dans-la-definition

Mise à jour du UserManager

public function getUserByConfirmationToken($token)
    {
        return $this->userRepository->findOneByConfirmationToken($token);
    }

Le ResetPasswordFormHandler

Comme d’habitude, un petit handler pour traiter la requête, le formulaire, et exécuter si besoin est des actions par des dépendances:

<?php

namespace AppBundle\User\Password;

use AppBundle\Form\Handler\FormHandlerInterface;
use AppBundle\User\Manager\UserManagerInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;

class ResetPasswordFormHandler implements FormHandlerInterface
{
    /**
     *
     * @var UserManagerInterface
     */
    private $handler;

    /**
     * @param UserManagerInterface $userManager
     */
    public function __construct(UserManagerInterface $userManager)
    {
        $this->handler = $userManager;
    }

    /**
     * @param FormInterface $form
     * @param Request       $request
     * @param array         $options
     *
     * @return bool
     */
    public function handle(FormInterface $form, Request $request, array $options = null)
    {
        $form->handleRequest($request);

        if (!$form->isValid()) {
            return false;
        }

        $token = $request->query->get('token');
        $user = $this->handler->getUserByConfirmationToken($token);
        
        $this->handler->clearConfirmationTokenUser($user);
        $this->handler->updateCredentials($user, $form->getData()->getPassword());

        return true;
    }
}

Si le formulaire est valide (et je m’en suis assuré dans le formType), alors je sais que j’ai bien un token et qu’il y a bien un user qui y est associé. Je peux donc faire appel au UserManager pour mettre à jour deux champs et changer le mot de passe en base avec la fonction updateCredentials.

Mise à jour du UserManager

Je lui rajoute donc ma fonction clearConfirmationToken($user):

public function clearConfirmationTokenUser(UserInterface $user) {
        $user->setConfirmationToken(null);
        $user->setIsAlreadyRequested(false);
}

Mais je ne flush pas car cela sera fait dans updateCredentials appelé juste après.

Mise à jour du fichier services.xml

<parameter key="app.reset_password.handler.class">AppBundle\User\Password\ResetPasswordFormHandler</parameter>

<service id="app.reset_password.handler"
                 class="%app.reset_password.handler.class%">
    <argument type="service" id="app.user.manager" />
</service>

 

Le ResetPasswordController

Il ne nous reste plus grand chose à faire. Le contrôleur va être important car il va catcher les erreurs que nous avons lancées dans le formType:

<?php

namespace AppBundle\Controller;

use AppBundle\User\Password\ResetPassword;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/")
 */
class ResetPasswordController extends Controller
{

    /**
     * @Route("/reset-password", name="reset_password")
     * @Method("GET|POST")
     */
    public function requestPasswordAction(Request $request)
    {
        try {
            $form = $this->createForm('reset_password_form', new ResetPassword());

            if ($this->getResetPasswordFormHandler()->handle($form, $request)) {
                $this->addFlash('success', 'Your password has been resetted. You can login now.');
                return $this->redirect($this->generateUrl('homepage'));
            }

            return $this->render('user/reset-password.html.twig',
                    [
                    'form' => $form->createView(),
            ]);
        } catch (\Exception $ex) {
            $this->addFlash('error', $ex->getMessage());
            return $this->redirect($this->generateUrl('security_login_form'));
        }
    }

    /**
     * @return \AppBundle\Form\Handler\FormHandlerInterface
     */
    protected function getResetPasswordFormHandler()
    {
        return $this->container->get('app.reset_password.handler');
    }
}

 

Le template

{# Resources/views/user/reset-password.html.twig #}
{% extends 'base.html.twig' %}

{% form_theme form "form_table_layout.html.twig" %}

{% block body_id 'reset-password' %}

{% block main %}
    <h1>Reset password</h1>
    {{ form(form) }}
{% endblock %}

 

Pour finir, j’ai créé des partials pour les messages flashes que j’ai placés au niveau des templates login, dashboard et homepage:

{# Resources/views/partials/errorSuccess.html.twig #}
{% for flashMessage in app.session.flashbag.get('error') %}
    <div class="alert alert-danger">
        {{ flashMessage }}
    </div>
{% endfor %}
{% for flashMessage in app.session.flashbag.get('success') %}
    <div class="flash-notice">
        {{ flashMessage }}
    </div>
{% endfor %}
{% include 'partials/errorSuccess.html.twig' %}

 

Le code github se trouve ici: https://github.com/jpsymfony/authentication-demo
Sa version mise à jour (il faudra chercher un peu pour retrouver les classes car l’arborescence a changé) en Symfony 3.2 est ici: https://github.com/jpsymfony/symfony3-generic-project-symfony3-architecture

Rédigé par

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.