How to create a simple application using Symfony and React Native – Part 3

How to create a simple application using Symfony and React Native – Part 3

In part 2 of this series we’ve added a custom endpoint to make it possible for users to register themselves. In this part we’ll add an EmailService class to send emails using the Symfony Messenger component. I’ll also show you an awesome technique for generating JWT Tokens for specific user actions like activating an account or resetting a password.

The Emailservice

Our application needs to send email messages to our users. A basic example is an account activation email with a link for users to click on. When they click on the link their account will be activated and ready for use.

To make this happen we’ll be creating an EmailService. I have covered this before in another article, so I wont go into details.

To create this service first we’ll need to install a few composer packages. Do so by running the following commands.

composer require symfony/mailer composer require symfony/messenger
Code language: Bash (bash)

Configure mailer

After the packages are installed we’ll need to configure them. In this series we’ll be using Gmail to send our emails for us. When you’re planning on using your application in production, use a specialized service for sending email messages like Sendgrid or MailGun. You can find the package names for those transports in the Symfony documentation.

Let’s require the google-mailer package to send our emails via Gmail.

composer require symfony/google-mailer
Code language: Bash (bash)

Next we need to configure the google-mailer package. Simply add the following to your .env.local file and replace YOUR-USERNAME and YOUR-PASSWORD with your Gmail username and password.

###> symfony/google-mailer ### MAILER_DSN=gmail://YOUR-USERNAME:[email protected] ###< symfony/google-mailer ###
Code language: Bash (bash)

We’re almost done configuring the Symfony Mailer. We’ll need to make a few more changes to the Symfony configuration. We don’t want to send the from address all the time, so let’s configure a default from address. Open the config/packages/mailer.yaml file and insert the following.

framework: mailer: dsn: '%env(MAILER_DSN)%' envelope: sender: '[email protected]' headers: from: 'Wouter <[email protected]>'
Code language: YAML (yaml)

To prevent test emails from getting sent to our users, we’ll configure the Symfony Mailer component to redirect all email to our email address when the application runs in development mode. Open (or create) config/packages/dev/mailer.yaml and add the following.

framework: mailer: envelope: recipients: ['[email protected]arabain.com']
Code language: YAML (yaml)

This configuration makes sure ALL emails sent from our application will be sent to [email protected]. No emails will accidently get sent to our users.

Configure messenger

To actually send the messages we’ll be using the Symfony Messenger component. This has a few advantages. One of them is that the register request is much faster since the application doesn’t have to wait on the email to be sent. Second advantage is the application will automatically retry when the message fails to send.

Let’s configure the messenger component. Start by adding the following to your .env.local file.

###> symfony/messenger ### MESSENGER_TRANSPORT_DSN=doctrine://default ###< symfony/messenger ###
Code language: Bash (bash)

This tells the Messenger component to use a Doctrine as the queue storage. When you expect a lot of messages or you start using the Messenger component for other tasks, it might be wise to use a specialized queueing system like RabbitMQ.

Now we’ve told the Messenger component to use Doctrine as the queue storage we can configure the message routing for the emails. Add the following to the config/packages/messenger.yaml file to redirect all emails to the message bus.

framework: messenger: transports: async: "%env(MESSENGER_TRANSPORT_DSN)%" routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async
Code language: YAML (yaml)

We’re done configuring the Messenger component. All of our emails will now be queued instead of sent immediately. To make sure the emails actually get sent we’ll need to run a command to consume the messenger queue. Run the following command to consume the queue.

bin/console messenger:consume async
Code language: Bash (bash)

This will start a command which will constantly check for new messages to process. It’s wise to create a linux service of this command or run it using supervisor so it will auto start and auto restart when it crashes.

Create the service class

Let’s create the actual service class. It’s basically the same as the one mentioned in our other post with a few modifications. Create src/Application/Service/EmailService.php and add the following code.

namespace App\Application\Service; use App\Entity\User; use Psr\Log\LoggerInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; /** * Service to send emails */ class EmailService { public function __construct( private MailerInterface $mailer, private LoggerInterface $logger ) {} /** * @param string $template * @param array|string $to * @param string $subject * @param array $templateVariables */ public function send(string $template, array|string $to, string $subject, array $templateVariables = []): void { $toAddress = is_array($to) ? new Address($to['address'], $to['name']) : new Address($to); $email = (new TemplatedEmail()) ->to($toAddress) ->subject($subject) ->context($templateVariables) ->htmlTemplate(sprintf('emails/%s.html.twig', $template)) ->textTemplate(sprintf('emails/%s.text.twig', $template)); try { $this->mailer->send($email); } catch (TransportExceptionInterface $e) { $this->logger->warning( sprintf('Cannot send email message: %s', $e->getMessage()), [ 'exception' => $e, 'template' => $template, 'to' => $to, 'subject' => $subject, 'templateVariables' => $templateVariables, ] ); } } /** * @param string $template * @param User $user * @param string $subject * @param array $templateVariables */ public function sendToUser(string $template, User $user, string $subject, array $templateVariables = []): void { $this->send( $template, ['address' => $user->getEmail(), 'name' => sprintf('%s %s', $user->getFirstName(), $user->getLastName())], $subject, $templateVariables ); } }
Code language: PHP (php)

We can inject this service and send email messages very easily wherever we need to send them.

Send welcome message

Let’s start sending actual messages. First we’ll need to create some templates. I’ve created 2 base templates which contain the basic layout for all emails sent from our application. For the HTML template I’ll use the one from https://github.com/leemunroe/responsive-html-email-template which I’ve adjusted a little bit to add a content block for Twig. I’ve created this HTML base template in templates/emails/base.html.twig. I also added a base template for the text variants of our emails. Because of the size of the HTML template I won’t show it in this article. You can find the file in my Github repository.

The text template goes in the file templates/emails/base.text.twig and has the following content.

{% block content %}{% endblock %} --- Don't like these emails? Unsubscribe at https://woutercarabain.com
Code language: Twig (twig)

Because we’ve created the base templates we can reference them in our specialized templates and ‘fill in the blocks’ of the base template. To extend a Twig template from another template you can use the extends keyword like so {% extends "emails/base.html.twig" %}.

To send the account activation email we need to create 2 templates. One for the HTML mail and one for the plaintext mail. I’ve created a templates/emails/account/welcome.html.twig file for the HTML version of the email and a templates/emails/account/welcome.text.twig file for the plaintext version.

The following code block are the HTML template and the plaintext template.

{% extends "emails/base.html.twig" %} {% block content %} <p>Hi {{ user.firstName }},</p> <p>Thanks for signing up for an account for the Stock Portfolio app. To activate your account please click the following button.</p> <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> <tbody> <tr> <td align="left"> <table role="presentation" border="0" cellpadding="0" cellspacing="0"> <tbody> <tr> <td> <a href="{{ activationLink }}" target="_blank">Activate account</a> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> <p>If you cannot click the link, copy and paste this in to your browser:</p> <p>{{ activationLink }}</p> <p>After activating your account you can use the service.</p> {% endblock %}
Code language: Twig (twig)
{% extends "emails/base.text.twig" %} {% block content %} Hi {{ user.firstName }}, Thanks for signing up for an account for the Stock Portfolio app. To activate your account please copy and paste the following link in your browser: {{ activationLink }} After activating your account you can use the service. {% endblock %}
Code language: Twig (twig)

The activation link

The account activation link needs a special secure token to make sure we activate the correct account. Because we already use JWT authentication in our application we can use the JWT tokens for the account activation links. These tokens are signed so there is no meddling with them without us noticing. They also remove the need to generate and store tokens in our database. We can also add extra data into the JWT payload like an action, so we can execute the correct action in our application. A win-win for us!

I just mentioned adding an action to the payload of the JWT tokens. So let’s create an action for activating an account. I like to define constants so we can reference the constants in our application instead of arbitrary string which can contain typo’s and might lead to bugs. Let’s create a JwtActions class to store the available JWT action constants. Start by creating src/Constant/JwtActions.php and add the following content.

namespace App\Constant; class JwtActions { public const ACTIVATE_ACCOUNT = 'activate_account'; }
Code language: PHP (php)

When you extend your API and want to use a link for password reset functionality you can simply add a PASSWORD_RESET constant here and use it in your password reset code.

Now, let’s update our UserService to actually make it send our welcome message. First we need to inject the EmailService we’ve just created. We also need to inject the JWTTokenManagerInterface so we can create our signed JWT tokens. Add the following code to your UserService.

namespace App\Application\Service; use App\Constant\JwtActions; use App\Dto\Request\User\RegisterUserRequest; use App\Entity\User; use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class UserService { public function __construct( private EntityManagerInterface $entityManager, private EmailService $emailService, private JWTTokenManagerInterface $tokenManager, private UserRepository $userRepository, private UserPasswordHasherInterface $userPasswordHasher ){} /** * @param string $email * * @return bool */ public function exists(string $email): bool { return null !== $this->userRepository->findOneBy(['email' => $email]); } /** * @param RegisterUserRequest $request * * @return User */ public function register(RegisterUserRequest $request): User { // Create the user here, removed for brevity, see other post / github for code $token = $this->tokenManager->createFromPayload($user, ['sub' => $user->getId(), 'action' => JwtActions::ACTIVATE_ACCOUNT]); $this->emailService->sendToUser('account/welcome', $user, 'Confirm your account', [ 'activationLink' => sprintf('https://stockportfolio.woutercarabain.com/activate-account?token=%s', $token), // TODO: Make this a parameter 'user' => $user, ]); return $user; } }
Code language: PHP (php)

This will generate an activation token and send the correct link to our user. There’s a TODO in the code for the hardcoded link which we’ll come back to later on in this series.

Add activate account endpoint

We’ve added the code to actually send the account activation email, so let’s create the account activation endpoint.

Validate the action JWT

To make sure the JWT is valid we’ll need to validate it. We can do this manually every time by calling the functions of the JWTTokenManagerInterface, or we can create a validator for it and let the Symfony Validator do the heavy lifting for us. I prefer the latter because it’s a lot easier to maintain. So let’s start creating our custom validator.

First we need to create a constraint which we can add to our token property later on. Create src/Validator/JwtToken.php and add the following content.

namespace App\Validator; use Symfony\Component\Validator\Constraint; /** * Validates the string as an JWT Token */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class JwtToken extends Constraint { public string|null $action = null; public string $invalidMessage = 'The token is invalid.'; public string $expiredMessage = 'The token is expired.'; public string $unverifiedMessage = 'The token is unverified.'; public string $noActionMessage = 'This token does not have an action.'; public string $differentActionMessage = 'This token does not have the correct action.'; public function __construct($options = null, array $groups = null, $payload = null, $action = null, $invalidMessage = null, $expiredMessage = null, $unverifiedMessage = null, $noActionMessage = null, $differentActionMessage = null) { parent::__construct($options, $groups, $payload); $this->action = $action ?? $this->action; $this->invalidMessage = $invalidMessage ?? $this->invalidMessage; $this->expiredMessage = $expiredMessage ?? $this->expiredMessage; $this->unverifiedMessage = $unverifiedMessage ?? $this->unverifiedMessage; $this->noActionMessage = $noActionMessage ?? $this->noActionMessage; $this->differentActionMessage = $differentActionMessage ?? $this->differentActionMessage; } }
Code language: PHP (php)

This defines the JwtToken attribute which we can add to the property of our soon to be created DTO. We need to specify the action we expect for the request. We can also override the default messages in the attribute if we need it.

Next we need to create the actual validator which will do the actual validation of the token. Create src/Validator/JwtTokenValidator.php with the following content.

namespace App\Validator; use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Validates the given JWT Token */ class JwtTokenValidator extends ConstraintValidator { /** * @param JWTTokenManagerInterface $tokenManager */ public function __construct(private JWTTokenManagerInterface $tokenManager) {} /** * @param mixed $value * @param Constraint $constraint */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof JwtToken) { throw new UnexpectedTypeException($constraint, JwtToken::class); } try { $token = $this->tokenManager->parse($value); if (null !== $constraint->action) { if (false === isset($token['action'])) { $this->context->buildViolation($constraint->noActionMessage) ->addViolation(); return; } if ($constraint->action !== $token['action']) { $this->context->buildViolation($constraint->differentActionMessage) ->addViolation(); return; } } } catch(JWTDecodeFailureException $e) { switch($e->getReason()) { case JWTDecodeFailureException::EXPIRED_TOKEN: $this->context->buildViolation($constraint->expiredMessage) ->addViolation(); return; case JWTDecodeFailureException::UNVERIFIED_TOKEN: $this->context->buildViolation($constraint->unverifiedMessage) ->addViolation(); return; case JWTDecodeFailureException::INVALID_TOKEN: default: $this->context->buildViolation($constraint->invalidMessage) ->addViolation(); return; } } } }
Code language: PHP (php)

This validator uses the JWTTokenManagerInterface to validate the token and will add a violation with the correct message if the token or the action is invalid.

Create the endpoint

Now that we’ve built the validator, we can start creating the endpoint. First let’s start by creating a DTO to store and validate the request data the client needs to send. In this case the client just needs to send the token, since it contains the user’s id and the action we need to perform. Create src/Dto/Request/User/ActivateAccountRequest.php and add the following content.

namespace App\Dto\Request\User; use App\Constant\JwtActions; use App\Validator\JwtToken; use Symfony\Component\Validator\Constraints as Assert; /** * Activate Account Request DTO */ class ActivateAccountRequest { #[Assert\NotBlank] #[JwtToken(action: JwtActions::ACTIVATE_ACCOUNT)] public string $token; }
Code language: PHP (php)

As you can see we’ve added a validation rule using the JwtToken constraint we’ve created earlier. For the action we use the JwtActions::ACTIVATE_ACCOUNT we’ve created earlier.

Now we can add a function to our existing UserService. For brevity I’ll just add the code for the function below.

/** * @param ActivateAccountRequest $request * * @return User * * @throws \Exception */ public function activate(ActivateAccountRequest $request): User { $decodedToken = $this->tokenManager->parse($request->token); $user = $this->userRepository->find($decodedToken['sub']); if (null === $user) { throw new \Exception(sprintf('The user %s was not found', $decodedToken['sub'])); } $user->setActive(true); return $user; }
Code language: PHP (php)

This function decodes the given token and gets the user’s id from the sub key of the decodedToken. Then it tries to fetch the User entity from the database. An Exception is thrown when the user entity was not found. If the user entity does exist, we set the active parameter to true using the setActive method of the User entity. Then we just return the user entity, because API Platform will do the database updating for us.

We’ve got the DTO and we’ve updated the UserService with a function to activate a user account. Now we can create the controller. Create src/Controller/User/ActivateAccount.php and add the following code.

namespace App\Controller\User; use App\Application\Service\UserService; use App\Entity\User; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; /** * Handles the activation of a new user */ class ActivateAccount extends AbstractController { /** * @param UserService $userService */ public function __construct(private UserService $userService) {} /** * @param Request $request * * @return User * * @throws \App\Exception\NotFoundException */ public function __invoke(Request $request): User { return $this->userService->activate($request->attributes->get('dto')); } }
Code language: PHP (php)

This controller just takes the request, gets the DTO from the request attributes and calls the activate function we just added to our UserService.

To add the actual endpoint we need to update our User entity. Add the following code to the ApiResource attribute of the UserEntity.

'activate' => [ 'method' => 'POST', 'status' => 204, 'path' => '/public/users/activate', 'controller' => ActivateAccount::class, 'defaults' => [ 'dto' => ActivateAccountRequest::class, ], 'openapi_context' => [ 'summary' => 'Activates a user account', 'description' => 'Activates a user account', 'requestBody' => [ 'content' => [ 'application/json' => [ 'schema' => [ 'type' => 'object', 'properties' => [ 'token' => [ 'type' => 'string', 'example' => 'TOKENSTRING', ], ], ], ], ] ], 'responses' => [ 204 => [ 'description' => 'The user is activated', ] ] ], 'read' => false, 'deserialize' => false, ],
Code language: PHP (php)

Make sure you add this below the closing tag of the register endpoint we created in part 2 of this series. Also make sure you add the use entries for the ActivateAccount controller and the ActivateAccountRequest DTO.

Thanks for reading

I won’t be adding the forget password and the reset password endpoints in this series. But it’s basically a rinse and repeat of the register and the activate account endpoints.

In part 4 we’re going to be tying up some loose ends. For example, the same email address can be used multiple times at the moment. We’re also going to add custom exceptions and we’ll make a start with the actual stock management endpoints! Stay tuned!

Leave a Reply

Your email address will not be published.