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.
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.
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.
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:
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.
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:
php bin/console config:dump symfonycasts_verify_email
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:
symfonycasts_verify_email:
lifetime: 7200 # default is 3600
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:
$user = $userRepository->find($request->query->get('id'));
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!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
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!