Overview
Views in Drupal are an excellent way to create blocks to be displayed on the page. Views comes with the ability to set access based on a permission through the UI, however sometimes a more customized set of access rules is required, rather than just a single permission. In this tutorial, two approaches will be looked at to provide custom access to a Drupal Views block.
Scenario
The scenario being looked at for this tutorial is as follows:
- A block has been created using Drupal Views, with the block ID:
some_views_block_id
(Note: You'll need to research your actual block ID) - The user has had a custom boolean field with a checkbox to the user profile. The field machine name is
show_view
- Users must have the permission
access_custom_user_view
to view the block - The user being viewed must have the
show_view
checkbox checked on their profile
Method 1: hook_block_access()
Implementing hook_block_access() is the easiest solution to implementing block access, and generally will meet the requirements to provide custom access for a block on the site. However, this hook is not called in every situation that the block is rendered. In the case that the site is using Layout Builder, and the block is added as an element of Layout using Layout Builder, rather than being added through the DOM, this hook is not called. When blocks are build programmatically, this hook is also not called. If you find you implement this method, and your access rules are not being respected, use a debugger to determine if this hook is being called, and if not, move on to method 2.
Here is how hook_block_access() can be implemented to meet the access requirements for the block, as outlined in the overview.
use Drupal\block\Entity\Block; use Drupal\Core\Access\AccessResult; use Drupal\Core\Session\AccountInterface; function HOOK_block_access(Block $block, $operation, AccountInterface Account) { if ($block->id() == 'some_views_block_id') { // Determine if on a user page. if ($user = \Drupal::service('current_route_match')->getParameter('user')) { // Provide access if the show_view field has been checked on the user profile, and // the viewing user has the access_custom_user_view permission return AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view')); } } return AccessResult::neutral(); }
Method 2: Custom Block Back-end
This method is more consistent than method 1 above, as it is called when rendering blocks through the admin interface, and can be called when rendering blocks programmatically (though it does not happen automatically, and must be called by the developer). With method 2, a custom class is created, extending the default ViewsBlock class that Views blocks use by default. There are two steps to this method:
- Create the new back-end class
- Tell Drupal that the block should use the extended class instead of the default class
Step 1: Create the new back-end class:
<?php namespace Drupal\example\Plugin\Block; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Routing\ResettableStackedRouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\views\Plugin\Block\ViewsBlock; use Drupal\views\ViewExecutableFactory; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Custom back end for user test results Views block, to provide custom access. * * This custom back end is implemented so that the block is only displayed to * users with the access_custom_user_view permission, on account pages where * the account has the show_view field checked. */ class UserTestResultsBlock extends ViewsBlock { /** * The current route match. * * @var \Drupal\Core\Routing\ResettableStackedRouteMatchInterface */ protected $currentRouteMatch; /** * Constructs a UserTestResultsBlock instance. * * @param array $configuration * A configuration array containing information about the plugin instance. * @param string $plugin_id * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. * @param \Drupal\views\ViewExecutableFactory $executable_factory * The view executable factory. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The views storage. * @param \Drupal\Core\Session\AccountInterface $user * The current user. * @param \Drupal\Core\Routing\ResettableStackedRouteMatchInterface $currentRouteMatch * The current route match. */ public function __construct( $plugin_id, $plugin_definition, ViewExecutableFactory $executable_factory, EntityStorageInterface $storage, AccountInterface $user, ResettableStackedRouteMatchInterface $currentRouteMatch ) { parent::__construct($configuration, $plugin_id, $plugin_definition, $executable_factory, $storage, $user); $this->currentRouteMatch = $currentRouteMatch; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('views.executable'), $container->get('entity_type.manager')->getStorage('view'), $container->get('current_user'), $container->get('current_route_match') ); } /** * {@inheritdoc} * * This implementation overrides the parent to hide the block for any user * that is not a student, as non-students will never have test results to * display. */ public function access(AccountInterface $account, $return_as_object = FALSE) { // Deny access by default. $access = AccessResult::forbidden(); // Determine if on a user page. if ($user = $this->currentRouteMatch->getParameter('user')) { // Provide access if the requirements are met. $access = AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view')); } // This function can return an AccessResult object, or the value it represents. return $return_as_object ? $access : $access->isAllowed(); } }
Step 2: Tell Drupal that the block should use the extended class instead of the default class
Next, Drupal needs to be told to use the custom back end, instead of the default ViewsBlock
class. This is done by implementing hook_block_alter():
/** * Implements hook_block_alter(). * * Swaps out the back-end of the some_views_block_id block and use a custom back-end * that provides a custom access callback. */ $definitions['iews_block:view_id-display_id'']['class'] = 'Drupal\example\Plugin\Block\UserTestResultsBlock'; } }
Summary
This tutorial discussed two methods to add custom access handlers to a Views block in Drupal. Note that the second method can also be used to add custom cache tags, cache context, or max-age to blocks created by other modules or by core, by implementing the getCacheTags()
, getCacheContexts()
and/or getMaxAge
methods in the custom block class.
Complete Code
Method 1
use Drupal\block\Entity\Block; use Drupal\Core\Access\AccessResult; use Drupal\Core\Session\AccountInterface; function HOOK_block_access(Block $block, $operation, AccountInterface Account) { if ($block->id() == 'some_views_block_id') { // Determine if on a user page. if ($user = \Drupal::service('current_route_match')->getParameter('user')) { // Provide access if the show_view field has been checked on the user profile. return AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view')); } } return AccessResult::neutral(); }
Method 2
<?php namespace Drupal\example\Plugin\Block; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Routing\ResettableStackedRouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\views\Plugin\Block\ViewsBlock; use Drupal\views\ViewExecutableFactory; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Custom back end for user test results Views block, to provide custom access. * * This custom back end is implemented so that the block is only displayed to * users with the access_custom_user_view permission, on account pages where * the account has the show_view field checked. */ class UserTestResultsBlock extends ViewsBlock { /** * The current route match. * * @var \Drupal\Core\Routing\ResettableStackedRouteMatchInterface */ protected $currentRouteMatch; /** * Constructs a UserTestResultsBlock instance. * * @param array $configuration * A configuration array containing information about the plugin instance. * @param string $plugin_id * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. * @param \Drupal\views\ViewExecutableFactory $executable_factory * The view executable factory. * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The views storage. * @param \Drupal\Core\Session\AccountInterface $user * The current user. * @param \Drupal\Core\Routing\ResettableStackedRouteMatchInterface $currentRouteMatch * The current route match. */ public function __construct( $plugin_id, $plugin_definition, ViewExecutableFactory $executable_factory, EntityStorageInterface $storage, AccountInterface $user, ResettableStackedRouteMatchInterface $currentRouteMatch ) { parent::__construct($configuration, $plugin_id, $plugin_definition, $executable_factory, $storage, $user); $this->currentRouteMatch = $currentRouteMatch; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('views.executable'), $container->get('entity_type.manager')->getStorage('view'), $container->get('current_user'), $container->get('current_route_match') ); } /** * {@inheritdoc} * * This implementation overrides the parent to hide the block for any user * that is not a student, as non-students will never have test results to * display. */ public function access(AccountInterface $account, $return_as_object = FALSE) { // Deny access by default. $access = AccessResult::forbidden(); // Determine if on a user page. if ($user = $this->currentRouteMatch->getParameter('user')) { // Provide access if the requirements are met. $access = AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view')); } // This function can return an AccessResult object, or the value it represents. return $return_as_object ? $access : $access->isAllowed(); } }
/** * Implements hook_block_alter(). * * Swaps out the back-end of the some_views_block_id block and use a custom back-end * that provides a custom access callback. */ $definitions['iews_block:view_id-display_id'']['class'] = 'Drupal\example\Plugin\Block\UserTestResultsBlock'; } }