When writing Symfony applications, you may find that you need to reuse some of your code over and over. A common example of this would be when writing separate controllers to manage different entity types in a REST API, where you need to authenticate the authorization header token for each request. Rather than including this method in each controller, this reusable code can be moved to a service which can be injected into all of your controllers as a dependency. With Symfony's autowiring enabled, adding a service is a quick and easy solution to avoid repeating code.
Creating a new Authorization Service
You can create a service class anywhere under your src/ directory. These are often stored in src/Service for example. Let's say we wanted to create an authorization service for creating and testing JWT tokens for your REST API. Start by creating a new file src/Service/AuthorizationService.php and adding a namespace and a class. We know we'll be needing the services for handing JWT tokens, so we can specify those with use statements as well.
(Do not put the extra spaces in the first line of code here.)
<?php
namespace App\Service;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class AuthorizationService
{
}
Testing your service
Once you've added the service to your controller, test to make sure it still works. This new code doesn't do anything yet, but your endpoints should still work if everything is correct.
Injecting AuthorizationService through the controller constructor
We can make your authorization service available to your controller by injecting it into the class constructor, which will execute every time your controller is initialized. Start by importing your service at the top of the constructor by adding use App\Service\AuthorizationService; with the other imports.
Add the following code to the API controller, preferably as the first method in your controller so it's easy to find.
public function __construct(
private readonly AuthorizationService $authService,
) {
}
Moving the authorization check to the service
In order to verify authorization for multiple endpoints, you've probably already created a method for this. This method probably reads the response Authorization header from the request, parses it, and decodes the payload with JWT::decode(). It's likely that your code also then retrieves the specified user object from the database to get more info about the user specified in the token. I'm not going to include all of the code for that here, because every project is different, but here are some common things you might need to do.
The Doctrine Entity Manager is already available in Symfony controllers. Since we created this new service from scratch, we will need to inject that as service by following the same process we applied above to inject a service using the class constructor.
At the top of your new service, add use Doctrine\ORM\EntityManagerInterface; to the list, then add the following code to inject and store the entity manager.
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {
}
Now you can copy your authorization method from your API controller to the service. Make sure to bring along any necessary use statements from the top of the controller file. When you need to retrieve a User record, you can use a query like this:
$user = $this->entityManager->getRepository(User::class)->find($userId);
Optional: Move JWT Token creation to AuthorizationService
If you've already followed the JWT tokens tutorial linked above, you probably already have code that uses the User object and the JWT_SECRET environment variable to create a token. Let's duplicate that code here. Don't forget to add use App\Entity\User; to the top of your file.
Note: copy your working code to generate a token here. This is just an example, and your code might be slightly different!
# Creates a token from the user account information.
public function getToken(User $user): string
{
$key = $_ENV["JWT_SECRET"];
$payload = [
'sub' => $user->getId(),
'iat' => time(),
'exp' => time() + 3600,
];
return JWT::encode($payload, $key, 'HS256');
}
Now, in your API controller, you can replace the code that creates your token with this service. Start by adding use App\Service\AuthorizationService; to the top of your controller so it can find your service. You'll want to inject this new service as a dependency to your existing login() method, then call the getToken() method of your new service to replace your existing code.
#[Route('/api/login', name: 'user_login')]
public function index(#[CurrentUser] ?User $user, AuthorizationService $authService): Response
{
// Some of your existing code.
$token = $authService->getToken($user);
// The rest of your existing code.
}
Test your API login, and if everything got moved around correctly, your API login should still work the same.
You may wonder why we moved this functionality, since it's only used once in your application. One reason is because doing this is a pretty easy test, but a bigger benefit is that we can now move all JWT-related code out of your controller and into your new service. Before we do that, let's make one more change to make your new service available at anywhere in your controller.
Now remove $authService as an argument to your login() method, and modify the line that generates the $token value.
$token = $this->authService->getToken($user);
This stores the authorization service in $this->authService which can then be reused to call the authentication check for every endpoint in the controller.
Clean up
You can now go through your controller(s) and clean up any code that is no longer needed. All of your controllers can now benefit from the shared code.
You might be thinking that we still have to create a constructor to inject the service in every controller, and you're right. This duplication can be further reduced by creating a base controller that contains the repeated code, and then have all of your API controllers extend that class instead. That enhancement is beyond the scope of this tutorial.