Change record status: 
Introduced in branch: 

Drupal core's access checks — ranging from hooks to route access checkers — currently only returns accessibility information, not cacheability information.

Consequently, anything that uses access checking during rendering (most notably menus) is inherently uncacheable, because we don't know whether e.g. a link "foo" is accessible or not based on user, role, language, or whatnot. If we'd know that the accessibility result (e.g. "Foo is not accessible") is cacheable per role, for example, then we'd know we can cache this per role.

This issue introduced \Drupal\Core\Access\AccessResultInterface, which is the new return value used for all access checking in Drupal 8. The default implementation, \Drupal\Core\Access\AccessResult, also implements CacheableDependencyInterface and hence provides cacheability metadata. It's still possible to not provide any cacheability metadata if you want (by creating an alternative AccessResultInterface implementation yourself), but that will then of course negatively impact cacheability of your site.

This effectively allows every access check to provide cacheability metadata, the overwhelming majority of which is then cacheable. Notably, this allows entity access checks to be cacheable (typically per role)!

  1. Addition: \Drupal\Core\Access\AccessResultInterface — the entire patch is centered around this.
  2. Addition: \Drupal\Core\Access\AccessResult, which implements AccessResultInterface and CacheableDependencyInterface.
        if ($route->getRequirement('_access') === 'TRUE') {
          return static::ALLOW;
        elseif ($route->getRequirement('_access') === 'FALSE') {
          return static::KILL;
        else {
          return static::DENY;
        if ($route->getRequirement('_access') === 'TRUE') {
          return AccessResult::allowed();
        elseif ($route->getRequirement('_access') === 'FALSE') {
          return AccessResult::forbidden();
        else {
          return AccessResult::neutral();
  3. Deletion: \Drupal\Core\Access\AccessInterface — this only served as a home for the ALLOW/DENY/KILL constants, which were the possible return values in many cases before this patch. Since we now have AccessResultInterface instead, they've become obsolete.
  4. Converting all the access-checking logic to AccessResultInterfacethis clearly shows how inconsistent access checking is today in HEAD:
    1. TRUE/FALSEAccessResultInterface
      1. \Drupal\Core\Access\AccessManagerInterface::check()
      2. \Drupal\Core\Access\AccessManagerInterface::checkNamedRoute()
      3. \Drupal\Core\Entity\EntityAccessControlHandlerInterface::checkAccess()
      4. \Drupal\Core\Entity\EntityAccessControlHandlerInterface::checkCreateAccess()
      5. \Drupal\Core\Field\FieldItemListInterface::defaultAccess()
      6. \Drupal\content_translation\ContentTranslationHandlerInterface::getTranslationAccess()
      7. \Drupal\quickedit\Access\EditEntityFieldAccessCheckInterface::accessEditEntityField()
      8. shortcut_set_edit_access()
      9. shortcut_set_switch_access()
    2. AccessInterface::(ALLOW|DENY|KILL)AccessResultInterface:
      1. \Drupal\Core\Access\AccessibleInterface::access() (also affects FieldItemListInterface and EntityInterface, which extend this interface)
    3. NODE_ACCESS_ALLOW/NODE_ACCESS_DENY/NODE_ACCESS_IGNOREAccessResultInterface::allowed()/AccessResultInterface::forbidden()/AccessResultInterface::neutral():
      1. hook_node_access()
    4. TRUE/FALSE/NULLAccessResultInterface
      1. hook_block_access()
      2. hook_entity_access()
      3. hook_entity_create_access()
      4. hook_ENTITY_TYPE_access()
      5. hook_ENTITY_TYPE_create_access()
      6. hook_entity_field_access()
      7. \Drupal\node\NodeGrantDatabaseStorageInterface::access()
  5. The following access checking logic used to return FALSE (the equivalent of AccessResult::forbidden()) in case it didn't care about an operation (e.g. the file access control handler only handled the "download" operation and returned FALSE in all other cases). But this is bad, because it prevents contrib modules implementing access checking logic for those other operations. Therefore, now AccessResult::neutral() is returned, to allow contrib modules to affect the access checking logic, for the following:
    1. LanguageAccessControlHandler
    2. shortcut_set_edit_access()
    3. shortcut_set_switch_access()
    4. ShortcutSetAccessControlHandler
    5. UserAccessControlHandler
    6. FileAccessControlHandler.
    7. MenuLinkContentAccessControlHandler

Note: all objects/interfaces that are often used by developers have received a DX optimization: by default they will return a boolean, but you can tell them to provide you the richer/"raw" AccessResultInterface object. The following now all have an optional $return_as_object = FALSE parameter:

  • AccessManagerInterface::check()
  • AccessManagerInterface::checkNamedRoute()
  • AccessibleInterface::access()
  • EntityAccessControlHandlerInterface::access()
  • EntityAccessControlHandlerInterface::createAccess()
  • EntityAccessControlHandlerInterface::fieldAccess()

And they all then do:

return $return_as_object ? $result : $result->isAllowed();

(where $result instanceof AccessResultInterface)

Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other updates done


Mile23’s picture

I had to look through the patch to find this, so here's a comment...

NODE_ACCESS_IGNORE is replaced with AccessResult::create(). That is, just create a result object and don't do anything to it.

This seems to be because the AccessResult::DENY value is actually the ignore value. To forbid access use AccessResult::forbidden().

See the change to Examples here: