26/03/2023

1. Modification du profil utilisateur

Lu 1177 fois Licence Creative Commons

Formulaire

Créer un premier formulaire dédié à la modification du profil de l'utilisateur courant:

php ./bin/console make:form
[output]
[output] The name of the form class (e.g. GentlePuppyType):
 > User\ProfileFormType
[output]
[output] The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
 > User
[output]
[output] created: src/Form/User/ProfileFormType.php

En indiquant à la 2e question que le formulaire devait être "lié" à l'entité User, la classe de formulaire générée aura déjà des champs correspondant à l'entité. Mais aussi, le formulaire retournera une instance de User.

Ouvrez la classe générée. On y trouve une méthode configureOptions() dans laquelle définir les options du formulaire. L'option data_class permet d'indiquer la classe de l'objet retourné par le formulaire. En son absence on obtient un tableau lorsque l'on récupère les données envoyées:

public function configureOptions(OptionsResolver $resolver): void
{
	$resolver->setDefaults([
		'data_class' => User::class,
	]);
}

L'autre méthode dans laquelle est décrite la structure du formulaire est buildForm(). Le form builder y est utilisé pour ajouter des champs:

public function buildForm(FormBuilderInterface $builder, array $options): void
{
	$builder
		->add('email')
		->add('roles')
		->add('password')
		->add('pseudo')
		->add('events')
	;
}

Commencer par supprimer les champs roles et events qui ne seront pas modifiables dans ce formulaire. Enfin, renommer password en plainPassword:

$builder
	->add('email')
	->add('pseudo')
	->add('plainPassword')
;

La propriété password dans l'entité User correspond au mot de passe hashé. Or, dans le formulaire, l'utilisateur va rentrer son mot de passe "en clair", que nous allons hasher par la suite. Nous allons voir comment séparer ce champ de formulaire de l'entité manipulée.

En laissant tel quel les champs, Symfony va tenter de deviner le type de champ à utiliser en fonction du type des propriétés dans User. Il est préférable d'indiquer explicitement les types en utilisant le 2e argument de la méthode add().
Les champs sont configurables par des options dont certaines sont spécifiques au type de champ. Les options sont passées en 3e argument de la méthode add() sous forme de tableaux. Parmi les options communes se trouve constraints qui permet de définir des contraintes de validation.
Le type et les options vont déterminer l'affichage du champ dans le template et la manière dont les données pourront être enregistrées.

Commençons par le champ email qui doit être un champ de type email et obligatoire:

use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;

// ...
	->add('email', EmailType::class, [
		'constraints' => [
			new NotBlank(['message' => 'Adresse email manquante.']),
			new Email(['message' => 'Adresse email invalide.']),
		],
	])
	// ...

Les contraintes de validation sont également configurables via des options passées dans le constructeur.

Ensuite le pseudo pour lequel on limitera la taille et les types de caractères utilisés:

use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Regex;

// ...
	->add('pseudo', TextType::class, [
		'constraints' => [
			new NotBlank(['message' => 'Pseudo manquant.']),
			new Length([
				'min' => 3,
				'minMessage' => 'Le pseudo doit faire au moins {{ limit }} caractères.',
				'max' => 30,
				'maxMessage' => 'Le pseudo ne doit pas dépasser {{ limit }} caractères.',
			]),
			new Regex([
				'pattern' => '~^[a-zA-Z0-9_.-]+$~',
				'message' => 'Le pseudo ne doit contenir que des caractères alphanumériques non accentués et ".", "-" et "_".',
			]),
		],
	])
	// ...

Note: les utilisations de {{ limit }} seront remplacées par les valeurs correspondantes. Plusieurs contraintes de validation permettent l'utilisation de ce type de placeholder.

Pour finir, le mot de passe, qui est facultatif si on ne souhaite modifier que les autres informations mais qui, sinon, doit être répété et qui est dissocié de l'entité User manipulée grâce à l'option mapped:

use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;

// ...
	->add('plainPassword', RepeatedType::class, [
		'type' => PasswordType::class,
		'invalid_message' => 'Les mots de passe ne correspondent pas.',
		'mapped' => false,
		'required' => false,
		'constraints' => [
			new Length([
				'min' => 8,
				'minMessage' => 'Le mot de passe doit faire au moins {{ limit }} caractères.',
				'max' => 4096,
				'maxMessage' => 'Le mot de passe ne doit pas dépasser {{ limit }} caractères.',
			])
		],
	])
	// ...

Entité

Il reste encore des contraintes de validations manquantes concernant l'email et le pseudo qui doivent être uniques à l'utilisateur. On va appliquer des contraintes UniqueEntity à l'entité User:

use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

// ...
#[UniqueEntity(
    fields: ['email'],
    message: 'Cette adresse email est déjà utilisée.',
)]
#[UniqueEntity(
    fields: ['pseudo'],
    message: 'Ce pseudo est déjà utilisé.',
)]
class User implements UserInterface, PasswordAuthenticatedUserInterface

Notez que les contraintes précédemment ajoutées dans le formulaire peuvent être affectées aux propriétés de l'entité.

Controlleur

Ouvrez le UserController afin de créer le formulaire qui traitera la requête pour récupérer les données POST:

public function index(Request $request): Response
{
	$form = $this->createForm(ProfileFormType::class, $this->getUser());
	$form->handleRequest($request);
    // ...
}

On vérifie ensuite si le formulaire a été envoyé et que les contraintes de validations ont été respectées. Dans ce cas, on récupère l'entité associée au formulaire avec getData() ainsi que le mot de passe "en clair" en ciblant spécifiquement le champ plainPassword:

public function index(Request $request): Response
{
	$form = $this->createForm(ProfileFormType::class, $this->getUser());
	$form->handleRequest($request);
	
	if ($form->isSubmitted() && $form->isValid()) {
		/** @var User $user */
		$user = $form->getData();
		$plainPassword = $form->get('plainPassword')->getData();
	}
    // ...
}

On peut ensuite hasher le mot de passe s'il a été modifié et mettre à jour l'utilisateur en base de données grâce à l'entity manager. L'entité étant mise à jour, elle a été récupérée de la base par Doctrine qui l'a enregistrée dans son identity map, il suffit alors d'appeler la méthode flush():

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

// ...

public function index(
	Request $request,
	UserPasswordHasherInterface $passwordHasher,
	EntityManagerInterface $entityManager
): Response {
	// ...
	if ($form->isSubmitted() && $form->isValid()) {
		// ...

		if (null !== $plainPassword) {
			$user->setPassword($passwordHasher->hashPassword($user, $plainPassword));
		}
		$entityManager->flush();
	}
    // ...
}

Note: pour des entités qu'il faudrait ajouter ou supprimer, il est nécessaire d'appeler les méthodes persist() ou remove() avant flush().

Enfin, pour avertir l'utilisateur que son action a bien réussi, on va ajouter un message flash. Il s'agit d'une notification à laquelle on affecte un type et qu'on affichera une seule fois à l'utilisateur sur la page:

if ($form->isSubmitted() && $form->isValid()) {
	// ...

	$this->addFlash('success', 'Votre profil a été mis à jour');
}

Pour finir, on doit passer un objet représentant le formulaire pour le template Twig:

return $this->render('user/index.html.twig', [
	'form' => $form->createView(),
]);

Template

Commençons par un template _includes/flashes.html.twig pour afficher les messages flash dans les pages. On récupère les messages via app.flashes qui retourne une liste de messages par leur type:

{% for type, message_list in app.flashes %}
    {% for message in message_list %}
        <div class="notification is-{{ type }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

Puis on ajoute le formulaire là où étaient simplement affichés l'email et le pseudo de l'utilisateur:

{% block content %}
    <div class="section">
		{# ... #}

        {% include '_includes/flashes.html.twig' %}

        <div class="content">
            {{ form_start(form) }}
                {{ form_row(form.email, {
                    label: 'Adresse email:'
                }) }}

                {{ form_row(form.pseudo, {
                    label: 'Pseudo:'
                }) }}

                {{ form_row(form.plainPassword.first, {
                    label: 'Mot de passe:'
                }) }}

                {{ form_row(form.plainPassword.second, {
                    label: 'Confirmez le mot de passe:'
                }) }}

                <button type="submit" class="button is-fullwidth is-primary">Mettre à jour</button>
            {{ form_end(form) }}

            <h3>Événements:</h3>
        </div>
		
		{# ... #}
{% endblock %}

Les fonctions form_start() et form_end() délimitent le début et la fin du formulaire et permettent d'ajouter automatiquement des champs qui seraient automatiques ou manquants.

La fonction form_row() permet d'afficher un champ complet, ce qui correspond à plusieurs composants dans une structure prédéfinie:

  • le label, affichable par form_label()
  • les erreurs de validation, affichables par form_errors()
  • le champ en lui-même, affichable par form_widget()
  • un message d'aide, affichable par form_help()

Comme dans la partie PHP, il est possible de passer un tableau d'options pour configurer l'affichage.

Vous pouvez désormais tester votre formulaire:

  • modifiez le pseudo
  • modifiez le mot de passe
  • entrez une adresse email déjà utilisée par un autre utilisateur
  • utilisez des caractères non autorisés dans le pseudo
  • ...

Note: La modification directe de l'entité de l'utilisateur courant peut poser certains problèmes et déconnecter l'utilisateur si l'identifiant (ici l'email) entré dans le formulaire est invalide. Pour éviter tout désagrément il serait préférable de lier le formulaire à une autre classe de type DTO.