Modifier son email
Dans notre entité User nous allons rajouter un attribut newEmail
ainsi que son code et sa date d'expiration.
/**
* @ORM\Column(type="string", length=180, nullable=true)
* @Groups("user:item:put")
* @Assert\Email(groups="update-email")
* @Assert\Length(max="180", groups="update-email")
* @AppAssert\IsEmailExists(groups="update-email")
*/
private $newEmail;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $newEmailCode;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $newEmailCodeExpiresAt;
// ...
public function getNewEmail(): ?string
{
return $this->newEmail;
}
public function setNewEmail(?string $newEmail): self
{
$this->newEmail = $newEmail;
return $this;
}
public function getNewEmailCode(): ?int
{
return $this->newEmailCode;
}
public function setNewEmailCode(?int $newEmailCode): self
{
$this->newEmailCode = $newEmailCode;
return $this;
}
public function getNewEmailCodeExpiresAt(): ?\DateTimeInterface
{
return $this->newEmailCodeExpiresAt;
}
public function setNewEmailCodeExpiresAt(?\DateTimeInterface $newEmailCodeExpiresAt): self
{
$this->newEmailCodeExpiresAt = $newEmailCodeExpiresAt;
return $this;
}
On adapte la logique de notre groupe de validation
public static function validationGroups(self $user)
{
if ($user->getCurrentPassword() !== null || $user->getPlainPassword() !== null) {
return ["update-password"];
}
if ($user->getNewEmail() !== null) {
return ["update-email"];
}
return [""];
}
On créé un validateur IsEmailExists
pour newEmail
qui va vérifier si le nouvel email n'est pas déjà enregistré sur notre base de donnée.
<?php
// src/Validator/Constraint/IsEmailExists.php
namespace App\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class IsEmailExists extends Constraint
{
public $message = 'This email is already registered in the database';
}
<?php
// src/Validator/Constraint/IsEmailExistsValidator.php
namespace App\Validator\Constraint;
use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class IsEmailExistsValidator extends ConstraintValidator
{
private $userRepository;
public function __construct(UserRepository $userRepository) {
$this->userRepository = $userRepository;
}
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof IsEmailExists) {
throw new UnexpectedTypeException($constraint, IsEmailExists::class);
}
if (null === $value || '' === $value) {
return;
}
$isEmailExists = $this->userRepository->findOneBy(['email' => $value]);
if ($isEmailExists) {
$this->context->buildViolation($constraint->message)
->addViolation()
;
}
}
}
On créé un nouveau service qui va nous permettre de créer le code et sa date d'expiration.
<?php
// src/Security/NewEmailCodeService.php
namespace App\Security;
use App\Entity\User;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Twig\Environment;
class NewEmailCodeService
{
private $mailer;
private $twig;
public function __construct(
MailerInterface $mailer,
Environment $twig
) {
$this->mailer = $mailer;
$this->twig = $twig;
}
public function generateCodeAndSendByMail(User $user) {
$newEmailCode = CodeService::generateNewCode();
$user->setNewEmailCode($newEmailCode['code']);
$user->setNewEmailCodeExpiresAt($newEmailCode['expirationDate']);
$email = (new Email())
->from('hello@example.com')
->to($user->getEmail())
->subject('Activez votre mail')
->html(
$this->twig->render('emails/new-email.html.twig', [
'newEmailCode' => $newEmailCode,
'newEmail' => $user->getNewEmail()
])
)
;
$this->mailer->send($email);
return $user;
}
}
Le template correspondant templates/emails/new-email.html.twig
<h3>Changement d'adresse email</h3>
<div>
Votre code de pour valider votre nouvelle adresse <strong>{{ newEmail }}</strong> est <strong>{{ newEmailCode.code }}</strong>
</div>
<div>
Ce code est valable jusqu'au <strong>{{ newEmailCode.expirationDate|date('Y-m-d H:i:s') }}</strong>
</div>
Nous allons détecter si un changement est fait sur l'attribut newEmail
dans notre UserDataPersister
et si c'est le cas on envoie un mail avec le code.
Pour ça nous allons utiliser l'entity manager, qui contient la méthode getUnitOfWork
. Dans cette méthode on va utiliser getOriginalEntityData
pour récupérer les données actuelle de l'utilisateur. Ensuite on va comparer avec ce qu'on envoie dans le payload. Il y a également la condition $data->getId() !== null
qui vérifie si notre utilisateur est déjà existant.
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\Entity\User;
use App\Security\ActivationCodeService;
use App\Security\NewEmailCodeService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Twig\Environment;
final class UserDataPersister implements ContextAwareDataPersisterInterface
{
private $decorated;
private $userPasswordEncoder;
private $mailer;
private $twig;
private $activationCodeService;
private $newEmailCodeService;
private $entityManager;
public function __construct(
ContextAwareDataPersisterInterface $decorated,
UserPasswordEncoderInterface $userPasswordEncoder,
MailerInterface $mailer,
Environment $twig,
ActivationCodeService $activationCodeService,
NewEmailCodeService $newEmailCodeService,
EntityManagerInterface $entityManager
) {
$this->decorated = $decorated;
$this->userPasswordEncoder = $userPasswordEncoder;
$this->mailer = $mailer;
$this->twig = $twig;
$this->activationCodeService = $activationCodeService;
$this->newEmailCodeService = $newEmailCodeService;
$this->entityManager = $entityManager;
}
public function supports($data, array $context = []): bool
{
return $this->decorated->supports($data, $context)
&& $data instanceof User;
}
/**
* @param User $data
* @param array $context
* @return object|void
* @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
*/
public function persist($data, array $context = [])
{
if ($data->getId() === null) {
if ($data->getPlainPassword()) {
$data->setPassword(
$this->userPasswordEncoder->encodePassword($data, $data->getPlainPassword())
);
$data->eraseCredentials();
}
$this->activationCodeService->generateCodeAndSendByMail($data);
}
$unitOfWork = $this->entityManager->getUnitOfWork();
$originalData = $unitOfWork->getOriginalEntityData($data);
if ($data->getId() !== null) {
if ($data->getNewEmail() && $originalData['newEmail'] !== $data->getNewEmail()) {
$this->newEmailCodeService->generateCodeAndSendByMail($data);
}
}
return $this->decorated->persist($data, $context);
}
public function remove($data, array $context = [])
{
return $this->decorated->remove($data, $context);
}
}
Maintenant à chaque fois qu'on modifie newEmail
avec la route PUT /users{id}
et le payload qui contient newEmail
:
{
"newEmail": "Elta2.Weissnat@yahoo.com"
}
Alors un email avec un nouveau code est envoyé.
Route pour valider le nouvel email
Nous allons dans notre entité User rajouter une nouvelle route
* "newEmailValidation"={
* "method"="POST",
* "path"="/new-email-validation",
* "input"=NewEmailCodeInput::class,
* "denormalization_context"={"groups"={"new-email:write"}},
* "security"="is_granted('ROLE_USER')"
* },
Nous avons besoin de créer NewEmailCodeInput
qui va vérifier si notre code est correct et non expiré et si l'email existe bien.
<?php
// src/DTO/NewEmailCodeInput.php
namespace App\DTO;
use App\Validator\Constraint\IsValidNewEmailCode;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @IsValidNewEmailCode()
*/
class NewEmailCodeInput
{
/**
* @Assert\NotBlank()
* @Assert\Length(6)
* @Assert\Type(type="integer")
* @Groups({"new-email:write"})
*/
public $code;
/**
* @Assert\NotBlank()
* @Assert\Email()
* @Groups({"new-email:write"})
*/
public $email;
}
Nous créons un validator IsValidNewEmailCode
qui va vérifier si les données rentrées sont correctes.
<?php
// src/Validator/Constraint
namespace App\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target({"CLASS"})
*/
class IsValidNewEmailCode extends Constraint
{
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
<?php
// src/Validator/Constraint/IsValidNewEmailCodeValidator.php
namespace App\Validator\Constraint;
use App\DTO\NewEmailCodeInput;
use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class IsValidNewEmailCodeValidator extends ConstraintValidator
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function validate($value, Constraint $constraint)
{
if (!$value instanceof NewEmailCodeInput) {
throw new \Exception('Only NewEmailCodeInput is supported');
}
$user = $this->userRepository->findOneBy(['email' => $value->email]);
if (null === $user) {
$this->context->buildViolation("Cet utilisateur n'existe pas")
->addViolation()
;
return;
}
if ($user->getNewEmailCodeExpiresAt() < new \DateTime()) {
$this->context->buildViolation("Ce code est expiré")
->addViolation()
;
return;
}
if ($value->code !== $user->getNewEmailCode()) {
$this->context->buildViolation("Ce code n'est pas correct")
->addViolation()
;
return;
}
}
}
On créé notre DataTransformer
<?php
namespace App\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use App\Entity\User;
class NewEmailDataTransformer implements DataTransformerInterface
{
public function transform($object, string $to, array $context = [])
{
return $object;
}
/**
* {@inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
if ($data instanceof User) {
return false;
}
return (User::class === $to)
&& null !== ($context['input']['class'] ?? null)
&& (array_key_exists('collection_operation_name', $context)
&& $context['collection_operation_name'] === 'newEmailValidation');
}
}
Si tout est valide on rentre dans notre DataPersister que nous devons créer NewEmailCodeDataPersister
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\NewEmailCodeInput;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
final class NewEmailCodeDataPersister implements ContextAwareDataPersisterInterface
{
private $userRepository;
private $entityManager;
public function __construct(
UserRepository $userRepository,
EntityManagerInterface $entityManager
) {
$this->userRepository = $userRepository;
$this->entityManager = $entityManager;
}
public function supports($data, array $context = []): bool
{
return $data instanceof NewEmailCodeInput;
}
/**
* @param NewEmailCodeInput $data
* @param array $context
* @return object|void
* @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
*/
public function persist($data, array $context = [])
{
$user = $this->userRepository->findOneBy(['email' => $data->email]);
$user->setEmail($user->getNewEmail());
$user->setNewEmailCode(null);
$user->setNewEmailCodeExpiresAt(null);
$user->setNewEmail(null);
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
}
public function remove($data, array $context = [])
{
}
}
Dans ce DataPersister nous récupérons l'utilisateur, et nous déplaçons newEmail
dans email
. Nous supprimons également les données dans newEmail
, newEmailCode
et newEmailCodeExpiresAt
afin que l'usage soit unique.
Pour tester la validation du nouveau mail vous pouvez aller sur la route POST /api/new-email-validation
avec comme payload
{
"email": "Elta.Weissnat@yahoo.com",
"code": 444462
}
Route pour générer un nouveau code s'il est expiré
Dans notre entité User nous créons cette route
* "resendNewEmailValidationCode"={
* "method"="POST",
* "path"="/resend-new-email-validation-code",
* "input"=ResendNewEmailCodeInput::class,
* "security"="is_granted('ROLE_USER')"
* },
Nous avons besoin d'un DTO input pour pouvoir mettre notre logique dans un DataPersister
Nous créons donc le le DTO suivant qui n'est qu'une classe vide
<?php
namespace App\DTO;
class ResendNewEmailCodeInput
{
}
Nous créons le DataTransformer pour ce DTO input
<?php
namespace App\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use App\Entity\User;
class ResendNewEmailDataTransformer implements DataTransformerInterface
{
public function transform($object, string $to, array $context = [])
{
return $object;
}
/**
* {@inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
if ($data instanceof User) {
return false;
}
return (User::class === $to)
&& null !== ($context['input']['class'] ?? null)
&& (array_key_exists('collection_operation_name', $context)
&& $context['collection_operation_name'] === 'resendNewEmailValidationCode');
}
}
Et enfin le DataPersister qui permet de renvoyer le code
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\ResendNewEmailCodeInput;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Security\NewEmailCodeService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Security;
final class ResendNewEmailValidationCodeDataPersister implements ContextAwareDataPersisterInterface
{
private $userRepository;
private $entityManager;
private $newEmailCodeService;
private $security;
public function __construct(
UserRepository $userRepository,
EntityManagerInterface $entityManager,
NewEmailCodeService $newEmailCodeService,
Security $security
) {
$this->userRepository = $userRepository;
$this->entityManager = $entityManager;
$this->newEmailCodeService = $newEmailCodeService;
$this->security = $security;
}
public function supports($data, array $context = []): bool
{
return $data instanceof ResendNewEmailCodeInput;
}
/**
* @param ResendNewEmailCodeInput $data
* @param array $context
* @return object|void
* @throws \Symfony\Component\Mailer\Exception\TransportExceptionInterface
*/
public function persist($data, array $context = [])
{
/** @var User $user */
$user = $this->security->getUser();
if ($user->getNewEmail()) {
$this->newEmailCodeService->generateCodeAndSendByMail($user);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return new JsonResponse(['message' => 'A new code has just been sent to you']);
}
public function remove($data, array $context = [])
{
}
}
Maintenant la route POST /api/resend-new-email-validation-code
avec le payload {}
génere un nouveau code et l'envoie par email.
Commentaires
Connectez-vous pour laisser un commentaire