We've seen how to create a service-based access checker that we can use on our routes. Using this technique, I want to demonstrate the flexibility of using the service on multiple routes. Imagine that we have multiple routes that display some user information. However, these routes are specific to a user type, and hence accessible only for that user type. In this example, a user type will be defined based on the value of a simple text field on the user entity, and we want to specify in the route definition for which user type it should be accessible. The code we write for this demonstration will go inside a new user_types module.
An alternative approach to checking the access inside a route for this example is to simply verify inside the Controller that the current user should access it. If not, throwing an AccessDeniedHttpException inside a Controller method will turn the request into a 403 (access denied). However, this is almost always the wrong approach because the route can no longer be verified for access, and we'll end up with links on our site that potentially lead to 403 pages. And we don't want that. For this reason, if the page has access rules, they belong in the access system and not in the Controller.
We'll go into this example with the assumption that the user entity has a field called field_user_type already on it; that we have users of three types: board_member, manager, and employee; and that we have the following four route definitions:
user_types.board_members: path: '/board-member' defaults: _controller: '\Drupal\user_types\Controller\UserTypesController::boardMember' _title: 'Board member' user_types.manager: path: '/manager' defaults: _controller: '\Drupal\user_types\Controller\UserTypesController::manager' _title: 'Manager' user_types.employee: path: '/employee' defaults: _controller: '\Drupal\user_types\Controller\UserTypesController::employee' _title: 'Employee' user_types.leadership: path: '/leadership' defaults: _controller: '\Drupal\user_types\Controller\UserTypesController::leadership' _title: 'Leadership'
These routes don't have any access requirements yet, as it is our job to create them now. However, you can already understand what kind of users should be able to access these routes. The user_types.board_members route is for board members, user_types.manager is for managers, user_types.employee is for both employees and managers (since both are actual employees), and user_types.leadership is for the board members and managers. So, a bit of mix and match to highlight the need for flexibility in our access checker.
Obviously, we don't want to write a service for each combination of user types to handle the access here. Using the static approach is not suitable either because we need to inject a dependency, and we also don't want to duplicate the logic using different callables.
So, let's define our service definition for this access checker:
user_types.access_checker: class: \Drupal\user_types\Access\UserTypesAccess arguments: ['@entity_type.manager'] tags: - { name: access_check, applies_to: _user_types_access_check }
We inject the entity type manager service so that we can load the user entity corresponding to the user whose access is being checked. As you remember, the AccountInterface is not enough to read field data from that user.
Now, we can update our route requirements (for all four routes) to make use of this access checker:
requirements: _user_types_access_check: 'TRUE'
Now, it's time to make the distinction between our four routes in terms of the types of users that should have access to them, and we can use route options for this. Options are a set of arbitrary pieces of data that we can put on a route definition and retrieve later programmatically. If you remember, in Chapter 2, Creating Your First Module, parameter converters are such an example that can be defined as an option in the route.
Let's take a look at just one of the routes as an example in full, and you'll extrapolate what the other routes will have to look like:
hello_world.employee: path: '/employee' defaults: _controller: '\Drupal\hello_world\Controller\UserTypesController::employee' _title: 'Employee' requirements: _user_types_access_check: 'TRUE' options: _user_types: - manager - employee
Route options are placed under the options key and are conventionally named with an underscore at the beginning (however, this is not mandatory). In a standard YAML notation, we have a sequence of string values underneath our _user_types option, which will be turned into a PHP array when read into the Route object.
Now, we can create our access checker service and make use of all this for controlling access:
namespace Drupal\user_types\Access; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityTypeManager; use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Session\AccountInterface; use Symfony\Component\Routing\Route; /** * Access handler for the User Types routes. */ class UserTypesAccess implements AccessInterface { /** * @var \Drupal\Core\Entity\EntityTypeManager */ protected $entityTypeManager; /** * UserTypesAccess constructor. * * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager */ public function __construct(EntityTypeManager $entityTypeManager) { $this->entityTypeManager = $entityTypeManager; } /** * Handles the access checking. * * @param AccountInterface $account * @param \Symfony\Component\Routing\Route $route * * @return \Drupal\Core\Access\AccessResult */ public function access(AccountInterface $account, Route $route) { $user_types = $route->getOption('_user_types'); if (!$user_types) { return AccessResult::forbidden(); } if ($account->isAnonymous()) { return AccessResult::forbidden(); } $user = $this->entityTypeManager->getStorage('user')->load($account->id()); $type = $user->get('field_user_type')->value; return in_array($type, $user_types) ? AccessResult::allowed() : AccessResult::forbidden(); } }
As per the service definition, we inject the entity type manager as a dependency. This is something we could not have done using the static approach. Then, in our access() method, we also type hint the route on which this service is used for evaluating access. Now comes the fun part.
We inspect the route and try to retrieve our option by name. Just as a fail-safe, we deny access if the option is missing. This should never be the case, as we only use this access checker on routes that do have the option, but you never know. Additionally, we also deny access if the user is anonymous. Anonymous users are sure not to have any user type field value.
Then, we load the user entity of the current account and simply check that field value and return access according to whether it is within the allowed ones for the route. I recommend that you inspect the Route class and see what other handy data you can make use of.
This is it. Now we have a flexible access-checking service that we can use on any number of routes that need this user type access control.
A key takeaway from this bonus technique is that you can build incredibly flexible architectures using options on routes. In this example, we used them for access, but you can also use them for other functionalities that tie to, and can be controlled from, the route.