Hey salut, bienvenue dans la suite de ce tutoriel sur Symfony 4. Dans les derniers tutoriels, nous avons créé des contrôleurs, des routes, afficher des vues, créer des entités, ajouter et modifier l'entité Article, récupérer nos entités en base de données, bref nous avons beaucoup appris ensemble (et j'espère que vous vous êtes bien amusés à le faire) et nous allons continuer dans ce sens.
Dans ce tutoriel, nous allons parler de l'authentification et de l'autorisation, puis nous verrons comment permettre à nos visiteurs de s'inscrire sur notre site à travers un formulaire.
Ça promet bien ce tutoriel, nous allons donc commencer maintenant. Mais avant, nous allons parler de ce qu'est l'authentification et l'autorisation.
Authentification vs Autorisation
J'ai vu plusieurs personnes qui confondent ces deux termes, qui sont totalement differents pourtant.
L'authentification (s'authentifier) veut dire confirmer son identité en fournissant par exemple ton nom d'utilisateur (username) ou ton email et ton mot de passe sur un système. Quand vous vous connectez sur Kaherecode par exemple à travers le formulaire de connexion, vous vous authentifiez et à partir de ce moment Kaherecode connait qui vous êtes, votre avatar est alors afficher sur la barre de navigation à droite.
Par contre l'autorisation c'est le fait de vous interdire l'accès à certaines pages ou vous l'autoriser. Sur Kaherecode par exemple, il y a un espace auquel vous seul avez accès, c'est votre espace, les tutoriels que vous créez personne d'autre n'y accès a part vous tant que ceux-ci ne sont pas publier, personne ne peut les modifier ou les supprimer. Il y a aussi un autre utilisateur qui lui a accès à la liste des utilisateurs qui sont inscrites sur le site, les tutoriels qui sont écrits et ainsi de suite, c'est l'administrateur au fait et il peut accorder ce rôle à d'autres utilisateurs s'il le veut. Il y a donc plusieurs niveau d'accès, et chaque utilisateur en fonction de son rôle, a accès à certaines pages spéciales.
Voilà donc la différence entre ces deux termes, l'Authentification c'est décliner son identité, et en fonction de ton identité on t'autorise ou pas à accéder à certaines pages du site.
Le composant Symfony Security
Ceux qui ont déjà utiliser les anciennes version de Symfony (Symfony 3 et avant) ont sûrement utiliser ou entendu parler de FOSUserBundle, c'est un bundle qui permet de faire la gestion des utilisateurs de bout en bout, il faut l'avouer il est vraiment extraordinaire et marche très bien, et il y a même d'autres développeurs qui l'utilisent avec Symfony 4, ça marche pas mal, mais pour l'avoir essayer je trouve qu'il ne colle absolument pas avec la philosophie derrière Symfony 4 et dont un principe particulièrement (Bundle-less). Symfony 4 ne supporte plus d'organiser son application en bundles et FOSUser est toujours dans cette logique malheureusement. Et puis Symfony aujourd'hui définit son propre système de sécurité performant et pratique et en tant que développeur tu as le contrôle sur absolument tout, plus besoin d'aller passer outre les contrôleurs de FOSUser pour ajouter ta logique, tu crées toi même ta logique au fait.
Pour commencer, nous allons d'abord créer notre entité User
. Les infos sur nos utilisateurs vont être enregistrés en base de données et c'est de là bas nous allons récupérer son login et mot de passe pour l'identifier. Pour créer l'entité User
, nous allons utiliser utiliser la commande suivante:
$ php bin/console make:user
Plusieurs questions vont donc nous être posés:
- Le nom de la classe, nous allons utiliser
User
- Ensuite si nous voulons enregistrer notre entité
User
dans une base de données? Oui vous répondez doncyes
- Puis le champ unique qui permettra d'identifier l'utilisateur, c'est avec cet identifiant qu'il va se connecter en plus de son mot de passe, nous allons choisir
email
- Et enfin est-ce que notre application doit crypté les mots de passe avant de les enregistrer? Evidemment que oui, ce n'est même pas une question et j'espère que je ne vous apprend rien disant qu'il ne faut jamais enregistrer un mot de passe qui n'est pas crypter, on répond donc
yes
- La dernière question est plutôt optionnelle, peut être que vous ne l'aurez même pas, cela demande juste quel algorithme de cryptage utiliser pour crypter le mot de passe
La classe User
a été créé dans le fichier src/Entity/User.php
// src/Entity/User.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
La classe User
hérite de UserInterface
, une interface créée par Symfony qui définit juste quelques fonctions de bases
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Core\User;
use Symfony\Component\Security\Core\Role\Role;
/**
* Represents the interface that all user classes must implement.
*
* This interface is useful because the authentication layer can deal with
* the object through its lifecycle, using the object to get the encoded
* password (for checking against a submitted password), assigning roles
* and so on.
*
* Regardless of how your user are loaded or where they come from (a database,
* configuration, web service, etc), you will have a class that implements
* this interface. Objects that implement this interface are created and
* loaded by different objects that implement UserProviderInterface
*
* @see UserProviderInterface
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface UserInterface
{
/**
* Returns the roles granted to the user.
*
* public function getRoles()
* {
* return array('ROLE_USER');
* }
*
* Alternatively, the roles might be stored on a ``roles`` property,
* and populated in any number of different ways when the user object
* is created.
*
* @return (Role|string)[] The user roles
*/
public function getRoles();
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* @return string The password
*/
public function getPassword();
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* @return string|null The salt
*/
public function getSalt();
/**
* Returns the username used to authenticate the user.
*
* @return string The username
*/
public function getUsername();
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials();
}
Nous pouvons utiliser la classe User
comme tel, mais moi j'aimerais rajouter un attribut username
pour avoir le nom d'utilisateur de la personne qui s'inscrit, peut être que dans d'autres applications, vous aurez besoin du prénom de l'utilisateur comme sur Kaherecode, ou de son téléphone, ... Nous allons donc utiliser la commande make:entity
pour regénérer la classe User
.
Maintenant que nous avons tous les attributs de notre entités, on peut créer la table dans notre base de données soit avec make:migration
ou doctrine:schema:update
, je préfère la deuxième (je m'y suis habituer bien avant MakerBundle).
$ php bin/console doctrine:schema:update --force
En plus de générer la classe User
, la commande make:user
a générer bien d'autres choses, comme le User Provider
qui va nous permettre de récupérer les informations de l'utilisateur depuis une session, configurer l'encodage du mot de passe, mais aussi definir un firewall (pare-feu). Toutes ces configurations se trouvent dans le fichier security.yaml
# config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: argon2i
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
Toute la configuration se trouve dans le bloc security
. Juste après, nous avons la clé encoders
qui contient le namespace de l'entité User
qui a son tour contient une clé algorithm
qui définit l'algorithme de cryptage de nos mot de passe. Cette valeur peut être différente dans votre cas, si vous avec une version de PHP inférieur à la 7.2, dans ce cas ce sera bcrypt
.
Après nous avons le bloc providers
qui définit un seul provider app_user_provider
. Ce provider utilise la classe User
que nous avons défini et l'attribut email
va servire d'identifiant à l'utilisateur pour se connecter. Nous pouvons changer cette valeur en un attribut que nous aurons défini dans la classe User
. Pour permettre à l'utilisateur de se connecter avec le username
au lieu de l'email par exemple, on aura plutôt:
# config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: argon2i
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: username
firewalls:
# ...
Et juste après, il y a les firewalls
(pare-feu) qui vont au fait définir le système d'authentification de notre application, comment est-ce que les utilisateurs vont s'authentifier? Ici il y a deux firewalls qui sont defini: dev
et main
. Le firewall dev
sert juste de se rassurer que nous ne bloquons pas par erreur les ressources de Symfony, car dans ce cas l'application ne tourne plus. Le firewall main
(principal) définit réellement l'accès a notre application, toute requête entrante passe par ce pare-feu, et ce pare-feu contient une clé anonymous
qui vaut true
, c'est ce qui nous permet d'accéder au site sans être authentifier. Si vous regardez la debugbar en bas sur la page d'accueil vous verrez que vous êtes actuellement authentifier en tant que anonymous
(anon.)
Essayer de mettre la valeur de anonymous
à false
et vous comprendrez mieux (il faut oser casser les choses et ensuite corriger pour mieux comprendre).
Ensuite il y a le bloc access_control
qui definit qui a accès à quel route. Nous le verrons plus tard.
Inscrire un utilisateur
Nous allons maintenant commencer par permettre à nos utilisateurs de pouvoir s'inscrire sur notre site. Nous avons déjà notre entité User
, ce que nous allons donc faire, c'est d'abord de créer un formulaire d'inscription où l'utilisateur va devoir renseigner ces informations en fonction de notre entité User
, puis nous allons créer un contrôleur pour traiter le formulaire et enregistrer l'utilisateur si tout va bien.
Vous connaissez déjà les formulaires, nous en avons parlé dans les parties précédentes. En temps normal, pour créer un formulaire, on utilise la commande:
$ php bin/console make:form
Mais pour le formulaire d'inscription, nous avons une commande spéciale qui va nous permettre de générer le formulaire d'inscription, son contrôleur et aussi la vue (le graal quoi), c'est la commande:
$ php bin/console make:registration-form
Répondez par yes
aux deux questions qui vous seront posées.
La première question s'assure qu'un utilisateur ne s'inscrit pas avec une même adresse email (ou tout autre attribut que nous avions choisi comme étant l'identifiant au moment de la création de l'entité User).
La deuxième permet de connecter l'utilisateur dès que celui-ci est inscrit. S'il faut forcément que l'utilisateur confirme son adresse email apres inscription, on aurait répondu non à cette question, mais ce n'est pas notre cas ici.
Cette commande va non seulement créer le formulaire RegistrationFormType.php
dans src/Form/
mais aussi le contrôleur RegistrationController.php
dans src/Controller/
et aussi la vue register.html.twig
dans templates/registration/
. Si vous vous rendez sur http://127.0.0.1:8000/register vous devez avoir une page avec le formulaire d'inscription.
Bizarre, je n'ai rien sur ma page.
Regardons donc le fichier register.html.twig
ce qu'il contient:
{# templates/registration/register.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block body %}
<h1>Register</h1>
{{ form_start(registrationForm) }}
{{ form_row(registrationForm.email) }}
{{ form_row(registrationForm.plainPassword) }}
{{ form_row(registrationForm.agreeTerms) }}
<button class="btn">Register</button>
{{ form_end(registrationForm) }}
{% endblock %}
Cette vue hérite de base.html.twig
, mais moi dans mon fichier base.html.body
je n'ai pas defini de block body
, c'est pour cela que ce contenu ne s'y affiche pas, l'équivalent du block body
ici c'est le block content
dans la vue base.html.twig
, je vais donc remplacer body par content:
{# templates/registration/register.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block content %}
<h1>Register</h1>
{{ form_start(registrationForm) }}
{{ form_row(registrationForm.email) }}
{{ form_row(registrationForm.plainPassword) }}
{{ form_row(registrationForm.agreeTerms) }}
<button class="btn">Register</button>
{{ form_end(registrationForm) }}
{% endblock %}
Je réactualise la page dans mon navigateur:
Et là biiiiimmmmm! J'ai mon formulaire qui s'affiche.
Avant de revenir sur le design, voyons les deux autres fichiers qui ont été générés, le contrôleur RegistrationController.php
dans src/Controller/
// src/Controller/RegistrationController.php
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class RegistrationController extends AbstractController
{
/**
* @Route("/register", name="app_register")
*/
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// do anything else you need here, like send an email
return $this->redirectToRoute('');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
}
C'est exactement un contrôleur comme on les connait déjà, ils utilisent une annotation pour définir la route app_register
pour le contrôleur register
, il traite le formulaire et si c'est bon l'utilisateur est persister et enregistrer en base de données avec l'entity manager, mais avant son mot de passe est d'abord crypter (il ne faut jamais enregistrer un mot de passe en base de données sans le crypter). Donc ça vous connaissez.
Il y a aussi le fichier RegistrationFormType.php
dans src/Form/
:
// src/Form/RegistrationFormType.php
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
])
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
Ce fichier définit le formulaire à afficher, ici les trois champs du formulaire sont email
, agreeTerms
et plainPassword
.
Nous n'avons pas besoin de agreeTerms
, nous allons donc le retirer.
Sur l'ajout du champ plainPassword
, il y a un commentaire qui dit que ce champ ne sera pas directement rattacher à l'objet (User
), c'est pour cela mapped
est à false
, la valeur de ce champ sera récupérée dans le contrôleur pour l'encoder puis l'attribuer à l'utilisateur avec $user->setPassword($encoded)
.
Il reste ici un attribut de notre entité que nous n'avons pas mentionner, c'est le username
, nous allons donc l'ajouter au formulaire, puis rajouter le field type au champ email
. Vous savez déjà le faire, je vous laisse donc vous débrouillez.
...
Voici mon fichier RegistrationFormType.php
// src/Form/RegistrationFormType.php
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('username', TextType::class, [
'constraints' => [
new Length([
'min' => 2,
'minMessage' => 'Votre nom d\'utilisateur doit contenir au moins {{ limit }} caractères.',
]),
],
])
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'constraints' => [
new NotBlank([
'message' => 'Veuillez saisir un mot de passe valide.',
]),
new Length([
'min' => 6,
'minMessage' => 'Votre mot de passe doit contenir au moins {{ limit }} caractères.',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
J'ai rajouter le champ username
de type texte, et un nom d'utilisateur doit contenir au moins 2 caractères, puis j'ai traduis les messages d'erreurs en français.
Et la vue register.html.twig
pour afficher le formulaire
{# templates/registration/register.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Inscription{% endblock %}
{% block content %}
<div class="container center">
<h1>Inscription</h1>
{{ form_start(registrationForm, {'attr': {'class': 'row'}}) }}
<div class="input-field col s12 m6 l6 offset-m3 offset-l3">
{{ form_widget(registrationForm.email, {'attr': {'placeholder': 'Adresse email'}}) }}
<span class="error">{{ form_errors(registrationForm.email) }}</span>
</div>
<div class="input-field col s12 m6 l6 offset-m3 offset-l3">
{{ form_widget(registrationForm.username, {'attr': {'placeholder': 'Nom d\'utilisateur'}}) }}
<span class="error">{{ form_errors(registrationForm.username) }}</span>
</div>
<div class="input-field col s12 m6 l6 offset-m3 offset-l3">
{{ form_widget(registrationForm.plainPassword, {'attr': {'placeholder': 'Mot de passe'}}) }}
<span class="error">{{ form_errors(registrationForm.plainPassword) }}</span>
</div>
<div class="input-field col s12">
<input type="submit" name="" class="btn btn-primary" value="M'inscrire">
</div>
{{ form_end(registrationForm) }}
</div>
{% endblock %}
Je n'ai écrit aucun code CSS, je me suis limiter à materialize, mais vous pouvez y mettre vos talents de designers pour faire une page exceptionnelle. Et voici comment le formulaire s'affiche:
Je vais essayer de m'inscrire avec un nom d'utilisateur qui contient un seul caractère:
J'ai une erreur qui me dit que le nom d'utilisateur doit être d'au moins 2 caractères.
Je vais maintenant m'inscrire réellement:
J'ai une erreur de redirection, normalement apres l'inscription, nous devons être rediriger vers la page d'accueil, mais là il y a un problème, nous regardons donc dans le controlleur RegistrationController.php
:
// src/Controller/RegistrationController.php
<?php
namespace App\Controller;
// ...
class RegistrationController extends AbstractController
{
/**
* @Route("/register", name="app_register")
*/
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
// ...
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// do anything else you need here, like send an email
return $this->redirectToRoute('homepage');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
}
Le problème se trouvait sur la ligne 39, je l'ai donc modifier pour rediriger l'utilisateur vers la page d'accueil.
Nous allons donc essayer pour voir ce qui va se passer:
Nous avons une erreur qui dit qu'un utilisateur existe déjà avec cet adresse email, ce qui s'est passé précédemment, c'est que l'utilisateur a été enregistrer en base de données avant d'avoir l'erreur de redirection, si vous regardez dans la base de données, vous verrez qu'un utilisateur existe déjà:
Avant de continuer, pour modifier le message d'erreur et le mettre en français, il faut vous rendre dans le fichier User.php
et le modifier comme ceci:
// src/Entity/User.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @UniqueEntity(fields={"email"}, message="Un utilisateur existe déjà avec cette adresse email.")
*/
class User implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
// ...
}
Le texte à modifier se trouve sur la ligne 11. Il faut savoir qu'il y a une autre méthode plus recommander de traduire automatiquement ces messages que nous ne pourrons pas voir ici. Vous pourrez en savoir plus sur la documentation officielle de Symfony.
Si vous essayez maintenant, vous avez bien le message d'erreur en français.
Nous allons donc inscrire un nouvel utilisateur pour vérifier que tout marche bien.
Yesssssss! Chez moi tout marche nickel. Ah que je suis content, et l'utilisateur existe bien dans ma base de données.
Et voilà, nos utilisateurs qui visiteront notre blog pourront s'inscrire sans problème.
Désolé, mais nous allons nous limiter là pour ce tutoriel, next step nous allons gérer la connexion et contrôler les différents accès, puis nous verrons comment automatiquement connecté l'utilisateur après l'inscription. D'ici là, essayez de revoir tout ce que nous avons vu et de le refaire encore et encore, y a que la pratique qui prime dans ce domaine. Merci et à bientôt.
Participe à la discussion