Custom Access on Views Blocks in Drupal

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.

  1. use Drupal\block\Entity\Block;
  2. use Drupal\Core\Access\AccessResult;
  3. use Drupal\Core\Session\AccountInterface;
  4.  
  5. function HOOK_block_access(Block $block, $operation, AccountInterface Account) {
  6. if ($block->id() == 'some_views_block_id') {
  7. // Determine if on a user page.
  8. if ($user = \Drupal::service('current_route_match')->getParameter('user')) {
  9. // Provide access if the show_view field has been checked on the user profile, and
  10. // the viewing user has the access_custom_user_view permission
  11. return AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view'));
  12. }
  13. }
  14.  
  15. return AccessResult::neutral();
  16. }

 

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:

  1. Create the new back-end class
  2. Tell Drupal that the block should use the extended class instead of the default class

Step 1: Create the new back-end class:

  1. <?php
  2.  
  3. namespace Drupal\example\Plugin\Block;
  4.  
  5. use Drupal\Core\Access\AccessResult;
  6. use Drupal\Core\Entity\EntityStorageInterface;
  7. use Drupal\Core\Routing\ResettableStackedRouteMatchInterface;
  8. use Drupal\Core\Session\AccountInterface;
  9. use Drupal\views\Plugin\Block\ViewsBlock;
  10. use Drupal\views\ViewExecutableFactory;
  11. use Symfony\Component\DependencyInjection\ContainerInterface;
  12.  
  13. /**
  14.  * Custom back end for user test results Views block, to provide custom access.
  15.  *
  16.  * This custom back end is implemented so that the block is only displayed to
  17.  * users with the access_custom_user_view permission, on account pages where
  18.  * the account has the show_view field checked.
  19.  */
  20. class UserTestResultsBlock extends ViewsBlock {
  21.  
  22. /**
  23.   * The current route match.
  24.   *
  25.   * @var \Drupal\Core\Routing\ResettableStackedRouteMatchInterface
  26.   */
  27. protected $currentRouteMatch;
  28.  
  29. /**
  30.   * Constructs a UserTestResultsBlock instance.
  31.   *
  32.   * @param array $configuration
  33.   * A configuration array containing information about the plugin instance.
  34.   * @param string $plugin_id
  35.   * The plugin_id for the plugin instance.
  36.   * @param mixed $plugin_definition
  37.   * The plugin implementation definition.
  38.   * @param \Drupal\views\ViewExecutableFactory $executable_factory
  39.   * The view executable factory.
  40.   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
  41.   * The views storage.
  42.   * @param \Drupal\Core\Session\AccountInterface $user
  43.   * The current user.
  44.   * @param \Drupal\Core\Routing\ResettableStackedRouteMatchInterface $currentRouteMatch
  45.   * The current route match.
  46.   */
  47. public function __construct(
  48. array $configuration,
  49. $plugin_id,
  50. $plugin_definition,
  51. ViewExecutableFactory $executable_factory,
  52. EntityStorageInterface $storage,
  53. AccountInterface $user,
  54. ResettableStackedRouteMatchInterface $currentRouteMatch
  55. ) {
  56. parent::__construct($configuration, $plugin_id, $plugin_definition, $executable_factory, $storage, $user);
  57.  
  58. $this->currentRouteMatch = $currentRouteMatch;
  59. }
  60.  
  61. /**
  62.   * {@inheritdoc}
  63.   */
  64. public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  65. return new static(
  66. $configuration,
  67. $plugin_id,
  68. $plugin_definition,
  69. $container->get('views.executable'),
  70. $container->get('entity_type.manager')->getStorage('view'),
  71. $container->get('current_user'),
  72. $container->get('current_route_match')
  73. );
  74. }
  75.  
  76. /**
  77.   * {@inheritdoc}
  78.   *
  79.   * This implementation overrides the parent to hide the block for any user
  80.   * that is not a student, as non-students will never have test results to
  81.   * display.
  82.   */
  83. public function access(AccountInterface $account, $return_as_object = FALSE) {
  84. // Deny access by default.
  85. $access = AccessResult::forbidden();
  86.  
  87. // Determine if on a user page.
  88. if ($user = $this->currentRouteMatch->getParameter('user')) {
  89. // Provide access if the requirements are met.
  90. $access = AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view'));
  91. }
  92.  
  93. // This function can return an AccessResult object, or the value it represents.
  94. return $return_as_object ? $access : $access->isAllowed();
  95. }
  96.  
  97. }

 

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():

  1. /**
  2.  * Implements hook_block_alter().
  3.  *
  4.  * Swaps out the back-end of the some_views_block_id block and use a custom back-end
  5.  * that provides a custom access callback.
  6.  */
  7. function HOOK_block_alter(array &$definitions) {
  8. if (isset($definitions['views_block:view_id-display_id'])) {
  9. $definitions['iews_block:view_id-display_id'']['class'] = 'Drupal\example\Plugin\Block\UserTestResultsBlock';
  10. }
  11. }

 

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

  1. use Drupal\block\Entity\Block;
  2. use Drupal\Core\Access\AccessResult;
  3. use Drupal\Core\Session\AccountInterface;
  4.  
  5. function HOOK_block_access(Block $block, $operation, AccountInterface Account) {
  6. if ($block->id() == 'some_views_block_id') {
  7. // Determine if on a user page.
  8. if ($user = \Drupal::service('current_route_match')->getParameter('user')) {
  9. // Provide access if the show_view field has been checked on the user profile.
  10. return AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view'));
  11. }
  12. }
  13.  
  14. return AccessResult::neutral();
  15. }

 

Method 2

  1. <?php
  2.  
  3. namespace Drupal\example\Plugin\Block;
  4.  
  5. use Drupal\Core\Access\AccessResult;
  6. use Drupal\Core\Entity\EntityStorageInterface;
  7. use Drupal\Core\Routing\ResettableStackedRouteMatchInterface;
  8. use Drupal\Core\Session\AccountInterface;
  9. use Drupal\views\Plugin\Block\ViewsBlock;
  10. use Drupal\views\ViewExecutableFactory;
  11. use Symfony\Component\DependencyInjection\ContainerInterface;
  12.  
  13. /**
  14.  * Custom back end for user test results Views block, to provide custom access.
  15.  *
  16.  * This custom back end is implemented so that the block is only displayed to
  17.  * users with the access_custom_user_view permission, on account pages where
  18.  * the account has the show_view field checked.
  19.  */
  20. class UserTestResultsBlock extends ViewsBlock {
  21.  
  22. /**
  23.   * The current route match.
  24.   *
  25.   * @var \Drupal\Core\Routing\ResettableStackedRouteMatchInterface
  26.   */
  27. protected $currentRouteMatch;
  28.  
  29. /**
  30.   * Constructs a UserTestResultsBlock instance.
  31.   *
  32.   * @param array $configuration
  33.   * A configuration array containing information about the plugin instance.
  34.   * @param string $plugin_id
  35.   * The plugin_id for the plugin instance.
  36.   * @param mixed $plugin_definition
  37.   * The plugin implementation definition.
  38.   * @param \Drupal\views\ViewExecutableFactory $executable_factory
  39.   * The view executable factory.
  40.   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
  41.   * The views storage.
  42.   * @param \Drupal\Core\Session\AccountInterface $user
  43.   * The current user.
  44.   * @param \Drupal\Core\Routing\ResettableStackedRouteMatchInterface $currentRouteMatch
  45.   * The current route match.
  46.   */
  47. public function __construct(
  48. array $configuration,
  49. $plugin_id,
  50. $plugin_definition,
  51. ViewExecutableFactory $executable_factory,
  52. EntityStorageInterface $storage,
  53. AccountInterface $user,
  54. ResettableStackedRouteMatchInterface $currentRouteMatch
  55. ) {
  56. parent::__construct($configuration, $plugin_id, $plugin_definition, $executable_factory, $storage, $user);
  57.  
  58. $this->currentRouteMatch = $currentRouteMatch;
  59. }
  60.  
  61. /**
  62.   * {@inheritdoc}
  63.   */
  64. public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  65. return new static(
  66. $configuration,
  67. $plugin_id,
  68. $plugin_definition,
  69. $container->get('views.executable'),
  70. $container->get('entity_type.manager')->getStorage('view'),
  71. $container->get('current_user'),
  72. $container->get('current_route_match')
  73. );
  74. }
  75.  
  76. /**
  77.   * {@inheritdoc}
  78.   *
  79.   * This implementation overrides the parent to hide the block for any user
  80.   * that is not a student, as non-students will never have test results to
  81.   * display.
  82.   */
  83. public function access(AccountInterface $account, $return_as_object = FALSE) {
  84. // Deny access by default.
  85. $access = AccessResult::forbidden();
  86.  
  87. // Determine if on a user page.
  88. if ($user = $this->currentRouteMatch->getParameter('user')) {
  89. // Provide access if the requirements are met.
  90. $access = AccessResult::allowedIf($user->get('show_view')->value && $account->hasPermission('access_custom_user_view'));
  91. }
  92.  
  93. // This function can return an AccessResult object, or the value it represents.
  94. return $return_as_object ? $access : $access->isAllowed();
  95. }
  96.  
  97. }
  1. /**
  2.  * Implements hook_block_alter().
  3.  *
  4.  * Swaps out the back-end of the some_views_block_id block and use a custom back-end
  5.  * that provides a custom access callback.
  6.  */
  7. function HOOK_block_alter(array &$definitions) {
  8. if (isset($definitions['views_block:view_id-display_id'])) {
  9. $definitions['iews_block:view_id-display_id'']['class'] = 'Drupal\example\Plugin\Block\UserTestResultsBlock';
  10. }
  11. }