Skip to main content

Controlling Access to Drupal 8 Routes with Access Checks

In the previous post, I looked at how to put together a basic route controller in Drupal 8, and restrict access by specifying permissions. But there are my situations where basic permissions aren't enough.

In Drupal 7 we had procedural access callbacks. In Drupal 8 we now have AccessCheck services.

This post takes you through how to use the new AccessCheck interface to provide a custom access checker for your routes.

by kim.pepper /

Controlling Access with AccessCheck

In Using Drupal 8's new route controllersour previous example, we are only checking that a user has a certain permission to access the route. What if we have some custom logic to check? In Drupal 7 we would use an access callback. In Drupal 8 we need to create an implementation of AccessCheckInterface and define it as a service in the dependency injection container.

The first step is to modify our route configuration, and specify a special tag for our access callback in our trousers.routing.yml file. Let's assume our Trousers access check needs to assert that the user has legs.

In trousers.routing.yml file:

trousers_list:
  pattern: '/trousers'
  defaults:
    _content: '\Drupal\trousers\Controller\TrouserController::list'
  requirements:
    _access_trousers_has_legs: 'TRUE'

In the above, we have created a new requirement, _access_trousers_has_legs: TRUE

The next step is to create our service definition for our TrousersHasLegsAccessCheck class by adding the following in trousers.services.yml in the root directory of our module.

services:
  access_check.trousers_has_legs:
    class: Drupal\trousers\Access\TrousersHasLegsAccessCheck
    tags:
      - { name: access_check }

The above snippet defines a new service with the unique key access_check.trousers_has_legs. By convention, we name any service that is an access check with an access_check. prefix. Next we define the class that implements our AccessCheckInterface. And finally, we tag the service with access_check so Drupal can find it when it loads all access checks.

Like all Drupal 8 classes loaded with PSR-0 namespaces, we create our access check in a directory that matches the namespace. In our trousers module, this is trousers/lib/Drupal/trousers/Access/TrousersHasLegsAccessCheck.php

Our TrousersHasLegsAccessCheck.php access check class looks like this.

<?php

namespace Drupal\trousers\Access;

use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
class TrousersHasLegsAccessCheck implements StaticAccessCheckInterface {

  public function appliesTo() {
    return '_access_trousers_has_legs';
}
 
public function access(Route $route, Request $request, AccountInterface $account) {
    if (!$account->hasLegs()) { // check if a user has legs
      return static::DENY; // denied! No legs.
    }
    return static::ALLOW; // OK to access trousers
  }
}

The appliesTo() method checks to see whether this access check should apply. It does this by simply checking whether the current route has an _access_trousers_has_legs requirement. As we defined this in our trouser.routing.yml file earlier, this should match when our route gets fired.

The access() method is where all the action happens. It gets passed the current Route, Request and current user (AccountInterface) objects. As this access check is a service, you could also inject in a database connection here to do some custom lookups. (See Using Drupal 8's new route controllers for an example of dependency injection).

There are some useful contstants on the StaticAccessCheckInterface interface (e.g DENY, ALLOW) which you return to let the router component know whether to allow or deny access. As you can see, we only allow access to the trousers route, if the current user has legs.

There are more advanced access rules that I haven't gone into here, like combining multiple access checks on the same route.

Please feel free to post comments or questions!

UPDATE: AccessInterface now passes current user (AccountInterface) as a parameter, so we don't need to look it up!