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
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
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
<?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
The last part to note is that we added a cache context to our