Creating Custom Views Access Plugins

Learn how to add your own access rules to any View in Drupal, no matter how complicated or otherworldly.

By default, Drupal Views comes with several access plugins that will probably cover most of what you need, across various scenarios. You can restrict access by role or permission, and by default, a new view comes with the Permission plugin selected, with the “View published content” option.

Page settings in a view

Plugin options show up in the UI under Page Settings -> Access.

Views Access Plugin Options

What if you need something specific? Maybe you have created some of your own access rules that you can't distill to roles or permissions. Maybe you have a view called the Temple of Gozer and only two users can access it: the Gatekeeper or the Keymaster. (You work for a very strange company.)

Determining if someone is the Gatekeeper or the Keymaster involves a lot of rules and condition trees that I won’t detail here, but they are all encapsulated in a helper class that has methods isUserGatekeeper and isUserKeymaster, and each of these methods takes a parameter of Drupal\Core\Session\AccountInterface.

It looks something like this:

Class TempleOfGozerAccessHandler {
  public function isUserTheGatekeeper(AccountInterface $account) {
      // Code to figure it out
  }

  public function isUserTheKeymaster(AccountInterface $account) {
      // Code to figure it out
  }

  public function access(AccountInterface $account, Route $route) {
      return $this->isUserTheGatekeeper($account) || $this->isUserTheKeymaster($account);
  }
}

You’ll notice an extra access() method that also takes a Route object which is explained further below.

How do you get a view to use this class to determine access? You write a custom Views Access Plugin.

The Beginning of a Views Access Plugin

Most access plugins will extend the Drupal\views\Plugin\views\access\AccessPluginBase class, and will need to define at least three methods: summaryTitle(), access(), and alterRouteDefinition(). 

Since this is a plugin, your class needs an annotation to describe some metadata. Here is what our TempleOfGozerAccess plugin looks like:

/**
 * @ingroup views_access_plugins
 *
 * @ViewsAccess(
 *   id = "temple_of_gozer_access",
 *   title = @Translation("Temple of Gozer"),
 *   help = @Translation("Access will be granted to only the Gatekeeper or the Keymaster.")
 * )
 */
class TempleOfGozerAccess extends AccessPluginBase {

}

The summaryTitle() method returns what shows up in the Views UI, so it should return a descriptive name that distinguishes it from other access plugins.

The access() method implementation might look something like this:

public function access(AccountInterface $account) {
  return $this->templeAccess->isUserTheGatekeeper($account) || $this->templeAccess->isUserTheKeymaster($account);
}

With a method name like “access” in our access plugin, you might be tempted to think that’s all that matters, but you also need to implement alterRouteDefinition().

Altering the View Route

Why does this need to be done? Why is this extra method required?

Each of these methods handles different contexts within the Drupal system and different times when permissions and access need to be determined. The access() method is specifically called when a request is attempted on the view. The only thing called out during route discovery is the alterRouteDefinition(). Every time Drupal builds its route table, it calls this method as well. This shouldn’t happen very often because it's an expensive operation. 

At first, this can feel a little awkward, but it allows some flexibility.

The simplest implementation of alterRouteDefinition() would look like:

public function alterRouteDefinition(Route $route) {
  return TRUE;
} 

This allows every user to visit the Temple of Gozer view route, but then the access() method would be called, and they would not see any content. They could see the Temple, but the door would be shut in their faces, which might be what you want.

But what if you want to keep the existence of the Temple secret? You want the view accessible via a menu item, and that menu item is only visible to the Keymaster or Gatekeeper. In that case, you need to set the permissions at the route level, which is what determines the visibility of and access to menu items.

Here is our new alterRouteDefinition():

public function alterRouteDefinition(Route $route) {
  $route->setRequirement('_custom_access', 'temple_of_gozer.access_handler::access');
} 

This assumes that the TempleOfGozerAccessHandler class is defined as a service with the name of “temple_of_gozer.access_handler.”

Clear your caches, and your menu item pointing to the view will only be visible to the Gatekeeper or Keymaster.

Adding User-defined Options to the Access Plugin

The functioning view access plugin only allows access to exactly who you want based on your complicated business logic inside TempleOfGozerAccessHandler.

But now, another view needs to be created—one that uses the exact same access rules, but can only be accessed during a full moon. (Did I mention that this is a very strange company?).

Instead of creating a whole new access plugin, you can define some options to allow more flexibility, and your current access plugin can meet both use cases. You'll need to add the following methods to your TempleOfGozerAccess class:

 protected function defineOptions() {
   $options = parent::defineOptions();
   $options['restriction'] = ['default' => 'none'];

   return $options;
 }

 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
   parent::buildOptionsForm($form, $form_state);
   $form['restriction'] = [
     '#type' => 'select',
     '#title' => $this->t('Additional Restriction'),
     '#default_value' => $this->options['restriction'],
     '#options' => [
       'none' => $this->t('None'),
       'full_moon' => $this->('Full moon'),
     ],
   ];
 }
} 

When a user selects your access plugin in a view, it will now present them with this form, similar to how the Permission plugin will allow a user to select a permission.

For usability, you might want to update your summaryTitle() to take these new options and the addition of future ones into account:

 public function summaryTitle() {
   if (isset($this->options['restriction'])) {
     return $this->t('Temple of Gozer (Restriction: @restriction)', ['@restriction' => $this->options['restriction']]);
   }

   return $this->t('Temple of Gozer');
 }

The access function will look like this:

public function access(AccountInterface $account) {
  $full_moon = TRUE;
  if ($this->options['restriction'] === 'full_moon') {
	$full_moon = $this->templeAccess->isFullMoon();
  }

  return ($this->templeAccess->isUserTheGatekeeper($account) || $this->templeAccess->isUserTheKeymaster($account)) && $full_moon;
}

This is starting to get a little unwieldy, so if more options are added, you will want to think about encapsulating that logic somewhere else, but for now, it meets its purposes and is still easy to reason with and read.

At this point, you might also need this option passed through to the route. You can pass the selected option in the alterRouteDefinition() method, and then use it in the access handler.

public function alterRouteDefinition(Route $route) {
  $route->setRequirement('_custom_access', 'temple_of_gozer.access_handler::access');
  $route->setOption('_additional_restriction', $this->options['restriction']);
}

And then the TempleOfGozerAccessHandler::access():

public function access(AccountInterface $account, Route $route) {
  $full_moon = TRUE;
  if ($route->getOption('_additional_restriction') === 'full_moon') {
    $full_moon = $this->isFullMoon();
  }
  return ($this->isUserTheGatekeeper($account) || $this->isUserTheKeymaster($account)) && $full_moon;
}

You probably noticed that both the plugin access() method and the TempleOfGozerAccessHandler::access() method have very similar logic. That is something you'll want to refactor if things start to get more complicated. However, since the TempleOfGozerAccessHandler::access() callback used for the route takes a Route object as well as an AccountInterface, you can’t just use one function in place of the other. You can’t just...ahem...cross the streams, if you will.

Conclusion

You have survived this terrible homage to the movie Ghostbusters, so pat yourself on the back. But you should now have the tools required to build custom views access plugins for any need or occasion. If you want another example, the core Permission plugin is a great one, especially if you need to think about more granular caching related to your access rules.

And don’t worry. It’s totally safe to cross the streams*.

*Lullabot is not responsible for any catastrophe that results from crossing the streams.

Published in:

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!