Verificar el correo electrónico tras el registro
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEn algunos sitios, después del registro, tienes que verificar tu correo electrónico. Seguro que estás familiarizado con el proceso: te registras, te envían un enlace especial a tu correo electrónico, haces clic en ese enlace y ¡voilà! Tu correo electrónico está verificado. Si no haces clic en ese enlace, dependiendo del sitio, puede que no tengas acceso a ciertas secciones... o puede que no puedas entrar en absoluto. Eso es lo que vamos a hacer.
Cuando ejecutamos originalmente el comando make:registration-form, nos preguntó si queríamos generar un proceso de verificación por correo electrónico. Si hubiéramos dicho que sí, nos habría generado un código. Dijimos que no... para poder construirlo a mano, aprender un poco más sobre su funcionamiento y personalizar un poco las cosas.
Propiedad User.isVerified
Pero antes de pasar a enviar el correo electrónico de verificación, dentro de nuestra clase User, necesitamos alguna forma de rastrear si un usuario ha verificado o no su correo electrónico. Vamos a añadir un nuevo campo para ello. Ejecuta:
symfony console make:entity
Actualiza User, añade una propiedad isVerified, de tipo booleano, no anulable y... ¡perfecto! Dirígete a la clase. Veamos... aquí vamos: $isVerified:
| // ... lines 1 - 17 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 20 - 56 | |
| /** | |
| * @ORM\Column(type="boolean") | |
| */ | |
| private $isVerified; | |
| // ... lines 61 - 225 | |
| public function getIsVerified(): ?bool | |
| { | |
| return $this->isVerified; | |
| } | |
| public function setIsVerified(bool $isVerified): self | |
| { | |
| $this->isVerified = $isVerified; | |
| return $this; | |
| } | |
| } |
Pongamos por defecto esto en false:
| // ... lines 1 - 17 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 20 - 59 | |
| private $isVerified = false; | |
| // ... lines 61 - 236 | |
| } |
Bien, es hora de la migración:
symfony console make:migration
Ve a comprobarlo y... impresionante. Se ve exactamente como esperamos:
| // ... lines 1 - 4 | |
| namespace DoctrineMigrations; | |
| use Doctrine\DBAL\Schema\Schema; | |
| use Doctrine\Migrations\AbstractMigration; | |
| /** | |
| * Auto-generated Migration: Please modify to your needs! | |
| */ | |
| final class Version20211012235912 extends AbstractMigration | |
| { | |
| public function getDescription(): string | |
| { | |
| return ''; | |
| } | |
| public function up(Schema $schema): void | |
| { | |
| // this up() migration is auto-generated, please modify it to your needs | |
| $this->addSql('ALTER TABLE user ADD is_verified TINYINT(1) NOT NULL'); | |
| } | |
| public function down(Schema $schema): void | |
| { | |
| // this down() migration is auto-generated, please modify it to your needs | |
| $this->addSql('ALTER TABLE user DROP is_verified'); | |
| } | |
| } |
¡Ejecútalo!
symfony console doctrine:migrations:migrate
¡Precioso! Hagamos una cosa más relacionada con la base de datos. Dentro desrc/Factory/UserFactory.php, para hacer la vida más sencilla, pon $isVerified en true:
| // ... lines 1 - 29 | |
| final class UserFactory extends ModelFactory | |
| { | |
| // ... lines 32 - 40 | |
| protected function getDefaults(): array | |
| { | |
| return [ | |
| // ... lines 44 - 46 | |
| 'isVerified' => true, | |
| ]; | |
| } | |
| // ... lines 50 - 68 | |
| } |
Así, por defecto, se verificarán los usuarios de nuestras instalaciones. Pero no me preocuparé de recargar mis accesorios todavía.
¡Hola VerifyEmailBundle!
Bien: ¡ahora vamos a añadir el sistema de confirmación por correo electrónico! ¿Cómo? Aprovechando un bundle! En tu terminal, ejecuta
composer require symfonycasts/verify-email-bundle
¡Hey, los conozco! Este bundle nos proporciona un par de servicios que nos ayudarán a generar una URL firmada que incluiremos en el correo electrónico y que luego validará esa URL firmada cuando el usuario haga clic en ella. Para que esto funcione, abreRegistrationController. Ya tenemos nuestro método register() que funciona. Ahora necesitamos otro método. Añade la función pública verifyUserEmail(). Sobre ella, dale una ruta: @Route("/verify") con name="app_verify_email":
| // ... lines 1 - 14 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 17 - 53 | |
| /** | |
| * @Route("/verify", name="app_verify_email") | |
| */ | |
| public function verifyUserEmail(): Response | |
| { | |
| // TODO | |
| } | |
| } |
Cuando el usuario haga clic en el enlace "confirmar correo electrónico" en el correo electrónico que le enviamos, esta es la ruta y el controlador al que le llevará ese enlace. De momento lo dejaré vacío. Pero finalmente, su trabajo será validar la URL firmada, lo que demostrará que el usuario hizo clic en el enlace exacto que le enviamos.
Envío del correo electrónico de confirmación
Arriba, en la acción register(), es donde tenemos que enviar ese correo electrónico. Como he mencionado antes, puedes hacer diferentes cosas en tu sitio en función de si el correo electrónico del usuario está verificado o no. En nuestro sitio, vamos a impedir completamente que el usuario se registre hasta que su correo electrónico esté verificado. Así que voy a eliminar lo de $userAuthenticator:
| // ... lines 1 - 14 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 17 - 19 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response | |
| { | |
| // ... lines 22 - 25 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 27 - 39 | |
| $userAuthenticator->authenticateUser( | |
| $user, | |
| $formLoginAuthenticator, | |
| $request | |
| ); | |
| return $this->redirectToRoute('app_homepage'); | |
| } | |
| // ... lines 48 - 51 | |
| } | |
| // ... lines 53 - 60 | |
| } |
Y sustituirlo por la redirección original a app_homepage:
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 47 | |
| return $this->redirectToRoute('app_homepage'); | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
Arriba, podemos eliminar algunos argumentos.
Genial. Ahora tenemos que generar el enlace de confirmación del correo electrónico firmado y enviarlo al usuario. Para ello, autocablea un nuevo servicio de tipoVerifyEmailHelperInterface. Llámalo $verifyEmailHelper:
| // ... lines 1 - 11 | |
| use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface; | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
A continuación, después de guardar el usuario, vamos a generar esa URL firmada. Esto... parece un poco raro al principio. Digamos que $signatureComponents es igual a$verifyEmailHelper->generateSignature():
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 35 | |
| $entityManager->flush(); | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| // ... lines 39 - 42 | |
| ); | |
| // ... lines 44 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
El primer argumento es el nombre de la ruta de verificación. Para nosotros, será app_verify_email:
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 55 | |
| /** | |
| * @Route("/verify", name="app_verify_email") | |
| */ | |
| public function verifyUserEmail(): Response | |
| { | |
| // ... line 61 | |
| } | |
| } |
Así que lo pondré aquí. A continuación, el identificador del usuario - $user->getId() - y el correo electrónico del usuario,$user->getEmail():
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 37 | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| 'app_verify_email', | |
| $user->getId(), | |
| $user->getEmail(), | |
| // ... line 42 | |
| ); | |
| // ... lines 44 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
Ambos se utilizan para "firmar" la URL, lo que ayudará a demostrar que este usuario hizo clic en el enlace del correo electrónico que le enviamos:
Verificar el correo electrónico sin estar conectado
Pero ahora tenemos un punto de decisión. Hay dos formas diferentes de utilizar el VerifyEmailBundle. La primera es cuando, cuando el usuario hace clic en el enlace de confirmación del correo electrónico, esperas que haya iniciado la sesión. En esta situación, abajo enverifyUserEmail(), podemos utilizar $this->getUser() para averiguar quién está intentando verificar su correo electrónico y utilizarlo para ayudar a validar la URL firmada.
El otro modo, que es el que vamos a utilizar, es permitir que el usuario no esté conectado cuando haga clic en el enlace de confirmación de su correo electrónico. Con este modo, necesitamos pasar un array como argumento final para incluir el id del usuario:
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 37 | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| 'app_verify_email', | |
| $user->getId(), | |
| $user->getEmail(), | |
| ['id' => $user->getId()] | |
| ); | |
| // ... lines 44 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
El objetivo de este método generateSignature() es generar una URL firmada. Y gracias a este último argumento, esa URL contendrá ahora un parámetro de consulta id... que podemos utilizar abajo en el método verifyUserEmail() para consultar el User. Lo veremos pronto.
Llegados a este punto, en una aplicación real, tomarías esta cosa de $signatureComponents, la pasarías a una plantilla de correo electrónico, la usarías para renderizar el enlace y luego enviarías el correo. Pero esto no es un tutorial sobre el envío de correos electrónicos, aunque tenemos ese tutorial. Así que voy a tomar un atajo. En lugar de enviar un correo electrónico, di $this->addFlash('success') con un pequeño mensaje que diga: "Confirma tu correo electrónico en:" y luego la URL firmada. Puedes generar eso diciendo $signatureComponents->getSignedUrl():
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 37 | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| 'app_verify_email', | |
| $user->getId(), | |
| $user->getEmail(), | |
| ['id' => $user->getId()] | |
| ); | |
| // TODO: in a real app, send this as an email! | |
| $this->addFlash('success', 'Confirm your email at: '.$signatureComponents->getSignedUrl()); | |
| // ... lines 47 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
No hemos hablado de los mensajes flash. Son básicamente mensajes temporales que puedes poner en la sesión... y luego renderizarlos una vez. He puesto este mensaje en la categoría success. Gracias a esto, en templates/base.html.twig, justo después de la navegación -por lo que está en la parte superior de la página- podemos renderizar cualquier mensaje flash de success. Añade para flash in app.flashes() y luego busca esa clave success. Dentro, añade div con alert, alert-success y renderiza el mensaje:
| <html> | |
| // ... lines 3 - 14 | |
| <body | |
| // ... lines 16 - 81 | |
| {% for flash in app.flashes('success') %} | |
| <div class="alert alert-success">{{ flash }}</div> | |
| {% endfor %} | |
| {% block body %}{% endblock %} | |
| // ... lines 87 - 89 | |
| </body> | |
| </html> |
Esto del flash no tiene nada que ver con la confirmación del correo electrónico... es sólo una característica de Symfony que se utiliza más comúnmente cuando se manejan formularios. Pero es un buen atajo para ayudarnos a probar esto.
A continuación: ¡hagamos... eso! Probemos nuestro formulario de registro y veamos qué aspecto tiene esta URL firmada. A continuación, rellenaremos la lógica para verificar esa URL y confirmar a nuestro usuario.
11 Comments
Hey there!
I'm glad it was useful!
> it seems there isn't functionality regarding resending the verification email after expiration. There seems some open issues (#35, #50) for this enhancement in the repo, but there hasn't been an update on those yet. Do you think such functionality will be added to the bundle in the near future or do we need to implement it ourselves?
I'm not sure. jrushlow helps me maintain that bundle, but we're all pretty busy. The ideal situation would be if someone from the community could add it. But, we have an internal OSS issue tracker, and I'll add this to it :). I agree that it really IS something the bundle needs.
Cheers!
I was needed to "verify email" functionality and at 6:27 on the video where Ryan has made a shortcut of FLASH message instead of sending email I was a little upset.
And I really began to prepare for learning the "Symfony Mailer: Love Sending Emails Again" course.
But when I was exploring the other courses, I found a brilliant =)
There is an extension for this functionality with exactly this code in "What's new in Symfony6" course.
Here is the link with video of sending emails: https://symfonycasts.com/screencast/symfony6-upgrade/docker
Maybe it will be helpful if you need a quick explanation how to send emails.
Thank you, Ryan. Again... =)
Hey @Magnoom Thanks for sharing it with others. Cheers!
I am trying to use an actual email verification while registering. To make things simple, I have chosen to use a verification email right in the registration form maker.
In the the register controller, in the register function, I have a call to
$this->emailVerifier->sendEmailConfirmation. ThesendEmailConfirmationdoes use$this->mailer->send($email);, which I guess, sends the email.As the email sending provider, I have chose Brevo and downloded the corresponding bundle. In the
.envfile. I have setMAILER_DSNtobrevo+api://KEY@defaultwhere I have replace KEY by my Brevo API key. I have not received any email and, as a matter of fact, the created user has ais_verifiedkey set to 0.Should I modify
@defaultin the MAILER_DSN entry?At this stage, I am rather confused. I have had a look at the Sfcast course on the symfony mailer, bur it seems that in the course an actual email is never sent (it uses a bogus email service). Since I do not get the confirmation email for real, I cannot check if the verification link works.
TIA,
François
Hey Francois,
Make sure you send emails sync and not async. If you have async emails sending - the email messages most probably hit your message queue, and in this case you would need to run the Messenger worker to consume those messages from the queue and actually send them.
Also, for email debugging purposes, you can leverage Mailtrap service... or some software that you can spin locally like Mailcatcher or MailHog. With this you would not need to use paid services like Brevo to actually send emails for debugging.
Cheers!
Thanks!
Things worked after setting the following line in the
messenger.yamlfile:BTW, at Brevo, the free plan lets you send 300 emails per day, which lets you do some testing without credit card
Have a good day!
Hey Francois,
Yeah, making that sync should simplify things locally. Instead of doing it for specific messages, you can do it globally by setting the
MESSENGER_TRANSPORT_DSN=sync://- then all your async messages will work sync - easier for debugging locally :)And yeah, a free plan with some limitations is useful too :)
Cheers!
Can you please tell me how to debug and change as example token_expiration ?
Hey Mepcuk@
You want to change the lifetime of the token expiration? I realize that we don't document this! At your command line, run:
That should dump the example config that's allowed for this bundle. You'll find that you can (in any YAML file in config/packages - so just create a new one) say:
Let me know if this is what you were looking for :).
Cheers!
Hi, I have an issue with this function : verifyUserEmail.
I don't know how write this code:
$user = $userRepository->find();
$request->query->get('id');
OR
$user = $userRepository->find($request->query->get('id'));
Can someone help me please ?
Louis G.
Hey Louis Gellez!
The second is correct:
The
$request->query->get('id')returns the string ?id" query parameter (e.g. if the URL ended in ?id=5, this would return "5"). Then this is passed to the find() method, which finds the User with that id. You'll find this exact code in the "finish" directory of the code download. And once we record this chapter (actually I moved this content to the NEXT chapter today - https://symfonycasts.com/screencast/symfony-security/verify-signed-url ) then you will be able to see it clearly in the code blocks.Cheers!
"Houston: no signs of life"
Start the conversation!