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()
ouremove()
avantflush()
.
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.