Route-level access answers "can this visitor reach the form at all?" — but sometimes different visitors who all pass that first check should still see different things once they're in. A logged-in editor and a site administrator might both be allowed onto the same form, yet only the administrator should see a particular sensitive field. This lesson covers that finer-grained layer: per-element access checks written as a small custom helper method on your own form class.
What you'll learn in this lesson
- Why per-element access is a pattern you write yourself, not a Drupal API method you override
- How to combine
#accesswith permission checks on individual form elements - How this layers on top of, rather than replaces, route-level access from the previous lesson
A custom per-element access helper
namespace Drupal\my_module\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class SecureAccessForm extends FormBase {
public function getFormId() {
return 'secure_access_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form['restricted_data'] = [
'#type' => 'textfield',
'#title' => $this->t('Restricted Data'),
'#access' => $this->elementAccess('restricted_data'),
];
$form['admin_setting'] = [
'#type' => 'checkbox',
'#title' => $this->t('Admin Setting'),
'#access' => $this->elementAccess('admin_setting'),
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save Changes'),
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger()->addMessage($this->t('Form has been submitted!'));
}
/**
* Checks whether the current user may see a given form element.
*
* This is an ordinary method we wrote ourselves — not a Drupal Form API
* hook or interface method. You could name it anything.
*/
protected function elementAccess(string $element_key): bool {
$current_user = \Drupal::currentUser();
return match ($element_key) {
'restricted_data' => $current_user->hasPermission('access restricted data'),
'admin_setting' => $current_user->hasPermission('administer site configuration'),
default => TRUE,
};
}
}
getFormAccess() to elementAccess(), and it's explicitly called out as a plain custom method, not something you're "implementing" against a Drupal interface — no such getFormAccess() API exists in core. The underlying #access pattern itself is genuinely idiomatic and correct; only the framing of it as a built-in method was wrong. Also swapped in $this->messenger()->addMessage() for the dead drupal_set_message() call, and modernized the switch to a PHP match expression.This layers on top of route-level access, it doesn't replace it
Nothing here checks whether the visitor can reach /secure-access-form at all — that's still the route's requirements key from the previous lesson. What #access controls is whether one specific element renders once the visitor is already on the page. Setting #access => FALSE doesn't just visually hide a field either: Drupal strips it from processing entirely, so even a manually crafted POST request that includes a value for that field gets ignored on submission. That's a meaningfully stronger guarantee than hiding a field with CSS.
Defining the permissions this depends on
access restricted data:
title: 'Access Restricted Data'
description: 'Allow users to access restricted data fields.'
restrict access: TRUE
administer site configuration:
title: 'Administer Site Configuration'
description: 'Allows users to have administrative access to site configuration.'
restrict access: TRUE
Quick check: if a visitor without the
access restricted datapermission somehow submits a raw POST request that includes a value forrestricted_data, does it reach yoursubmitForm()? (Answer: no — because the element's#accessevaluated toFALSEduringbuildForm(), Drupal excludes it from the processed form values entirely, regardless of what the raw request contained.)
Key takeaways
- Route-level access (
_permission,_custom_access) controls whether a visitor reaches a form at all; per-element#accesscontrols what they see once they're on it — use both together, not one instead of the other. - There's no built-in Drupal method you "implement" for per-element access — write a plain method on your form class and call it from
buildForm(). #access => FALSEstrips an element from processing entirely, not just from the rendered HTML — a stronger guarantee than CSS-based hiding.drupal_set_message()is dead Drupal 7 code — use the Messenger service instead.
Coming up next
You've now covered the full security surface of Drupal forms — CSRF tokens, input sanitization, route-level permissions, file upload validation, and per-element access. Next, we shift focus from security to speed: caching form render arrays, so a form's expensive parts don't get rebuilt from scratch on every single request.