Form and View Modes vs. Field Access in Drupal 8

Gabe Sullice

Drupal 8 advertised many new, promising features after its release. One of the exciting new changes was the addition of form modes. Form modes promised to let you manage the content entry side of your site just as you often managed content display with view modes. This change seemed like it would eliminate the need for much of the custom and repetitive code I often needed to write inside a hook_form_alter.

Over time, I've realized that form modes aren't everything I had hoped they would be. While it's easy to create new form modes, it's literally impossible to use them without custom code or contributed modules. Drupal simply doesn't have a way to know when to use one form mode over another. Should it be based on role? Permissions? A field on the node? Content moderation state? There are contributed modules for most if not all of these, but nothing out-of-the-box.

This forced me to think about why I needed a form mode in the first place. Almost always, the answer was to disable or hide a field from a user because that user shouldn't be allowed to change that field. The same was also often true of my view modes (only to a lesser extent). I realized that this particular problem is not one of user experience, but of access control.

Drupal 8 has hook_entity_field_access(). This hook is called for every field for the specified entity type when the entity is viewed or when its form is shown. When you deny access to a field, either for viewing or editing, that field will not be shown to the user. In any scenario. This should be your preferred method for hiding fields that certain users should not be able to access.

Using field access over form and view modes to hide fields when a user should not be allowed to see or edit a field is the secure and "Drupal way" to do things. This prevents mistakes in configuration, which might accidentally leak field information via teasers, searches, and Views. It also future proofs your site. If you ever turn on REST or JSON API or add a new form or view mode down the line, you can never accidentally expose a field that needs to be kept private.

Best of all, using the field access hook is much easier to implement than all the hoops you'll have to jump through to get the right form modes displayed at the right times.

How to use hook_entity_field_access()

First, make a custom module in the standard way. Create a .module file and create the following function:

<?php
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Field\FieldItemListInterface;
 
 
/**
 * Implements hook_entity_field_access().
 */
function yourmodule_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
}
?>

From this hook, you should always return an AccessResult. By default, you should simply return a neutral access result. That is, your hook is not concerned with actually allowing or preventing access yet. Add the following to your function.

<?php
function yourmodule_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
    $result = AccessResult::neutral();
    if ($field_definition->getName() == 'field_we_care_about') {
        if (/* a condition we'll write later... */) {
            $result = AccessResult::forbidden();
        }
    }
    return $result;
}
?>

The above code will deny access when our still unwritten condition is true, in every other case, we're just saying "we don't care".

There's an infinite number of scenarios in which you might want to deny access, but let's say that we want to make a field only editable by an administrator. We would add the following:

<?php
function yourmodule_node_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
    $result = AccessResult::neutral();
    if ($field_definition->getName() == 'field_we_care_about') {
        if ($op == 'update' && !in_array('administator', $account->getRoles())) {
                $result = AccessResult::forbidden();
        }
    }
    return $result->addCacheContexts(['user.role:administrator']);
}
?>

Now, for every user without the administrator role that attempts to update field_we_care_about, the field will not be accessible. This works for more than just forms. For example, if we had the REST module installed, this would block the user from updating the field in that way as well.

The last part to note is that we added a cache context to our AccessResult. This ensures that our access decision is only relevant when the current user does or does not have the 'administrator' role. It's important to understand that we added the cache context both when we did and when we did not deny access. If we had just added the context when we denied access, if a user with the 'administrator' role happened to be the first person to attempt to access the field, then that result would be cached for all users no matter what.