The service approach involves creating a tagged service and referencing that in the route definition as a requirement. There are a number of advantages to this method compared to the one we've just seen:
- Allows you to encapsulate complex access logic in its own class
- Allows you to inject dependencies and make use of them in calculating the access
- Allows you to reuse the access checker on multiple routes
Let's take a look at how we can implement this for our Hello World route. We will replace the previous approach, but keep the goal of denying access to editors. However, to increase a bit complexity, editors will be allowed if the Hello World salutation has not been overridden via the configuration form. If you recall, in Chapter 2, Creating Your First Module, we created a form where the salutation message can be overridden and stored in a configuration object.
First, let's create our class. Typically, access-related classes go inside the Access folder of the module namespace—it's not necessarily so, but it makes sense to put them there. Then, we can have something like this:
namespace Drupal\hello_world\Access; use Drupal\Core\Access\AccessResult; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Session\AccountInterface; /** * Access handler for the Hello World route. */ class HelloWorldAccess implements AccessInterface { /** * @var \Drupal\Core\Config\ConfigFactoryInterface */ protected $configFactory; /** * HelloWorldAccess constructor. * * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory */ public function __construct(ConfigFactoryInterface $configFactory) { $this->configFactory = $configFactory; } /** * Handles the access checking. * * @param AccountInterface $account * * @return AccessResult */ public function access(AccountInterface $account) { $salutation = $this->configFactory->get('hello_world.custom_salutation')->get('salutation'); return in_array('editor', $account->getRoles()) && $salutation != "" ? AccessResult::forbidden() : AccessResult::allowed(); } }
Right off the bat, I would like to mention that the AccessInterface we're implementing is at this point a bit up in the air. If you look inside, you'll see that it has no methods. This is because of the dynamic argument resolving we talked about earlier, by which we can get the route and route match if we type-hint them. There was an ongoing discussion at the time of writing this book on marking it deprecated and maybe eventually removing it completely (or finding another solution). So, it's something worth paying attention to in the long run.
Also, since there is no interface, the access() method naming is not enforced. However, we will need it because that is the name being looked for by the access system when using the service. As before, we get the user making the request from which we can get the roles. Moreover, we injected the configuration factory and checked whether the salutation text had been overridden. Only if that is the case will editors be denied access. It's nothing too complicated for us at this point.
Now, let's take a look at how we define this as a service to be used by our route as an access checker:
hello_world.access_checker:
class: \Drupal\hello_world\Access\HelloWorldAccess
arguments: ['@config.factory']
tags:
- { name: access_check, applies_to: _hello_world_access_check }
As you can see, tagged services are very important in Drupal 8 and are a great example of an extension point with which we can contribute our own code to an existing set of functionality. In this example, apart from tagging it for access checking, we also see another option to this tag: applies_to. The corresponding string is what we can now use in our route definition to target this particular access checker. So instead of the following line:
_custom_access: '\Drupal\hello_world\Controller\HelloWorldController::access'
We have this one:
_hello_world_access_check: 'TRUE'
The TRUE value we set doesn't make much of a difference. If we wanted, we could add a string value that could actually be used by the access checker internally. However, we'll use a different approach for that later. So, for now, the standard thing to do is just use TRUE.
After clearing the cache, our new access checker will kick in and that is pretty much it.