The Form API
Our page displays a greeting dynamically, depending on the time of day. However, we now want an administrator to specify what the greeting should actually be, in other words, to override the default behavior of our salutation if they so choose.
The ingredients for achieving this will be as follows:
- A route (a new page) that displays a form where the administrator can set the greeting
- A configuration object that will store the greeting
In building this functionality, we will also take a look at how to add a dependency to our existing service. So, let's get started with our new route, which naturally goes inside the hello_world.routing.yml
file we already created:
hello_world.greeting_form: path: '/admin/config/salutation-configuration' defaults: _form: Drupal\hello_world\Form\SalutationConfigurationForm _title: 'Salutation configuration' requirements: _permission: 'administer site configuration'
Most of this route definition is the same as we saw earlier. There is one change, though, in that it maps to a form instead of a Controller. This means that the entire page is a form page. Also, since the path is within the administration space, it will use the administration theme of the site. What is left to do now is to create our form class inside the /Form
folder of our namespace (a standard practice directory for storing forms, but not mandatory).
Due to the power of inheritance, our form is actually very simple. However, I will explain what goes on in the background and guide you on your path to building more complex forms. So, here we have our form:
namespace Drupal\hello_world\Form; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; /** * Configuration form definition for the salutation message. */ class SalutationConfigurationForm extends ConfigFormBase { /** * {@inheritdoc} */ protected function getEditableConfigNames() { return ['hello_world.custom_salutation']; } /** * {@inheritdoc} */ public function getFormId() { return 'salutation_configuration_form'; } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { $config = $this->config('hello_world.custom_salutation'); $form['salutation'] = array( '#type' => 'textfield', '#title' => $this->t('Salutation'), '#description' => $this->t('Please provide the salutation you want to use.'), '#default_value' => $config->get('salutation'), ); return parent::buildForm($form, $form_state); } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->config('hello_world.custom_salutation') ->set('salutation', $form_state->getValue('salutation')) ->save(); parent::submitForm($form, $form_state); } }
Clearing the cache and navigating to admin/config/salutation-configuration
will present you with your simple configuration form via which you can save a custom salutation message:
Figure 2.3: Salutation configuration form
Later on, we will make use of that value. However, first, let's talk a bit about forms in general, and then this form in particular.
A form in Drupal is represented by a class that implements FormInterface
. Typically, we either extend from FormBase
or ConfigFormBase
, depending on what its purpose is. In this case, we created a configuration form, so we extended from the latter class.
There are four main methods that come into play in this interface:
getFormId()
: Returns a unique, machine-readable name for the form.buildForm()
: Returns the form definition (an array of form element definitions and some extra metadata, as needed).validateForm()
: The handler that gets called to validate the form submission. It receives the form definition and aFormStateInterface
object that contains, among others, the submitted values. You can flag invalid values on their respective form elements, which means that the form is not submitted but refreshed (with the offending elements highlighted).submitForm()
: The handler that gets called when the form is submitted (if validation has passed without errors). It receives the same arguments asvalidateForm()
. You can perform operations such as saving the submitted values or triggering some other kind of flow.
Defining a form, in a nutshell, means creating an array of form element definitions. The resulting form is very similar to the render array we mentioned earlier. When creating your forms, you have a large number of form element types to use. A complete reference of what they are and what their options are (their definition specificities) can be found on the Drupal Form API reference page (https://api.drupal.org/api/drupal/elements/9.0.x).
From a dependency injection point of view, forms can receive arguments from the service container in the same way that we injected the salutation service into our Controller. As a matter of fact, ConfigFormBase
, which we are extending in our example, injects the config.factory
service because it needs to use it for reading and storing configuration values. This is why we extend from that form. Drupal is full of these helpful classes that we can extend and that provide a bunch of useful boilerplate code that is very commonly used across the Drupal ecosystem.
If the form you are building is not storing or working with configuration, you will typically extend from FormBase
, which provides some static methods and traits and also implements some interfaces. The same word of caution goes for its helper service methods as for the ones of ControllerBase
: if you need services, you should always inject them.
Let's turn to our form class and dissect it a bit now that we know a thing or two about forms.
We have the getFormId()
method. Check. We also have buildForm()
and submitForm()
, but not validateForm()
. The latter is not mandatory, and we don't actually need it for our example, but if we did, we could have something like this:
/** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { $salutation = $form_state->getValue('salutation'); if (strlen($salutation) > 20) { $form_state->setErrorByName('salutation', $this->t('This salutation is too long')); } }
In this validation handler, we basically check whether the submitted value for the salutation
element is longer than 20 characters. If so, we set an error on that element (to turn it red usually) and specify an error message on the form state specific to this error. The form will then be refreshed and the error will be presented, and the submit handler, in this case, will not be called.
For the purposes of our example, this is, however, not necessary, so I will not include it in the final code.
Note
Form validation error messages, by default, are printed at the top of the page. However, with the core Inline Form Errors module, we can have the form errors printed right beneath the actual elements. This is much better for accessibility, as well as for clarity when dealing with large forms. Note that the standard Drupal 9 installation doesn't have this module enabled, so you'll have to enable it yourself if you want to use it.
If we turn back to our form class, we also see a strange getEditableConfigNames()
method. This is required by the ConfigFormBaseTrait
, which is used in the ConfigFormBase
class that we are extending. It needs to return an array of configuration object names that this form intends to edit. This is because there are two ways of loading configuration objects: for editing and for reading (immutable). With this method, we inform it that we want to edit that configuration item.
As we see on the first line of buildForm()
, we are using the config()
method of the previously mentioned trait to load up our editable configuration object from the Drupal configuration factory. This is to check the value that is currently stored in it. Then, we define our form elements (in our case, one—a simple text field). For #default_value
(the value present in the element when the user goes to the form), we put whatever is in the configuration object. The rest of the element options are self-explanatory and pretty standard across all element types. Consult the Form API reference to see what other options are available and for which element types. Finally, at the end of the method, we also call the parent method because that provides the form's submit button, which for our purposes is enough.
The last method we need is the submit handler, which basically loads up the editable configuration object, puts the submitted value in it, and then saves it. Finally, it also calls the parent method, which then simply sends a success message to the user on the screen using the Messenger
service—a standard way of showing the user a success or error message.
That is pretty much it; this will work just fine.
From the point of view of configuration, we used ConfigFormBase
to make our lives easier and combine the form aspect with that of the configuration storage. In a later chapter, we will talk more about the different types of storage and also cover how to work with configuration objects. So, no worries if you are left a bit unclear about how configuration works.
Altering forms
Before going ahead with our proposed functionality, I would like to open a parenthesis and discuss forms in a bit more detail. An important thing that you will do as a module developer is alter forms defined by other modules or Drupal core. So, it behooves us to talk about it early on and what better moment than now, when defining the form itself is still fresh in our minds.
Obviously, the form we just created belongs to us and we can change it however we want. However, many forms out there have been defined by other modules and there will be just as many times that you will want to make changes to them. Drupal provides us with a very flexible, albeit still procedural, way of doing so—a suite of alter hooks; but what are alter hooks?
The first thing we did in this chapter was implement hook_help()
. That is an example of an invoked hook by which a caller (Drupal core or any module) asks other modules to provide input. This input is then aggregated in some way and made use of. The other type of hooks we have in Drupal are the alter hooks, which are used to allow other modules to make changes to an array or an object before that array or object is used for whatever it is used for. So, in the case of forms, there are some alter hooks that allow modules to make changes to the form definition before it's processed for rendering.
You may be wondering why I am saying that to make changes to a form, we have more than one alter hook. Let me explain by giving an example of how other modules could alter the form we just defined (will not be included in our code base):
/** * Implements hook_form_alter(). */ function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { if ($form_id === 'salutation_configuration_form') { // Perform alterations. } }
In the code above, we implement the generic hook_form_alter()
, which gets fired for all forms when being built, and we do so inside a module called my_module
. The first two arguments are the form and form state (the same as we saw in the form definition), the former being passed by reference. This is the typical alter concept—we make changes to an existing variable and don't return anything. The third parameter is the form ID, the one we defined in the getFormId()
method of our form class. We check to ensure that the form is correct and then we can make alterations to the form.
This is, however, almost always the wrong approach, because the hook is fired for all forms indiscriminately. Even if we don't actually do anything for most of them, it's still a useless function call, not to mention that if we want to alter 10 forms in our module, there will be a lot of if
conditionals in there—the price we pay for procedural functions. Instead, though, we can do this:
/** * Implements hook_form_FORM_ID_alter(). */ function my_module_form_salutation_configuration_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { // Perform alterations. }
Here, we are implementing hook_form_FORM_ID_alter()
, which is a dynamic alter hook in that its name contains the actual ID of the form we want to alter. So, with this approach, we ensure that this function is called only when it's time to alter OUR form. The other benefit is that if we need to alter another form, we can implement the same kind of hook for that and have our logic neatly separated.
Custom submit handlers
So, up to now, we have seen how other modules can make changes to our form. That means adding new form elements, changing existing ones, and so on. But what about our validation and submit handlers (those methods that get called when the form is submitted). How can those be altered?
Typically, for the forms defined as we did, it's pretty simple. Once we alter the form and inspect the $form
array, we can find a #submit
key, which is an array that has one item: ::submitForm
. This is simply the submitForm()
method on the form class. So, what we can do is either remove this item and add our own function, or simply add another item to that array:
/** * Implements hook_form_FORM_ID_alter(). */ function my_module_form_salutation_configuration_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { // Perform alterations. $form['#submit'][] = 'my_module_salutation_configuration_form_submit'; }
And the callback we added to the #submit
array above can look like this:
/** * Custom submit handler for the form_salutation_configuration form. * * @param $form * @param \Drupal\Core\Form\FormStateInterface $form_state */ function my_module_salutation_configuration_form_submit(&$form, \Drupal\Core\Form\FormStateInterface $form_state) { // Do something when the form is submitted. }
So, the cool thing is that you can choose to tack on your own callback or replace the existing one. Keep in mind that the order they are located in that array is the order in which they get executed. So, you can also change the order if you want.
There is another case though. If the submit button on the form has a #submit
property specifying its own handler, the default form #submit
handlers we saw just now won't fire anymore. This was not the case with our form. So, in that situation, you will need to add your own handler to that form's submit element array instead.
Finally, when it comes to the validation handler, it works exactly the same as with the submit handler, but it all happens under the #validate
array key.
Feel free to experiment with altering existing forms and inspect the variables they receive as arguments.
Rendering forms
Staying on forms for just a bit longer, let's quickly learn how to render forms programmatically. We have already seen how to map a form to a route definition so that the page being built contains the form when accessing the route path. However, there are times when we need to render a form programmatically, either inside a Controller or a block, or wherever we want. We can do this using the FormBuilder
service.
The form builder can be injected using the form_builder
service key or used statically via the shorthand:
$builder = \Drupal::formBuilder();
Once we have it, we can build a form, like so:
$form = $builder->getForm('Drupal\hello_world\Form\SalutationConfigurationForm');
In the code above, $form
will be a render array of the form that we can return, for example, inside a Controller. We'll talk more about render arrays a bit later on, and you›ll understand how they get turned into actual form markup. However, for now, this is all you need to know about rendering forms programmatically—you get the form builder and request from it the form using the fully qualified name of the form class.
With this, we can close the parenthesis on forms.
Service dependencies
In the previous section, we created a form that allows administrators to set up a custom salutation message to be shown on the page. This message was stored in a configuration object that we can now load in our HelloWorldSalutation
service. So, let›s do just that with a two-step process.
First, we will need to alter our service definition to give our service an argument—the configuration factory (the service responsible for loading config objects). This is how our service definition should look now:
hello_world.salutation: class: Drupal\hello_world\HelloWorldSalutation arguments: ['@config.factory']
The addition is the arguments
key, which is an array of service names proceeded by @
. In this case, config.factory
is the responsible service name, which, if we check in the core.services.yml
file, we can note that it maps to the Drupal\Core\Config\ConfigFactory
class.
So, with this change, the HelloWorldSalutation
class will be passed an instance of ConfigFactory
. All we need to do now is adjust our class to actually receive it:
/** * @var \Drupal\Core\Config\ConfigFactoryInterface */ protected $configFactory; /** * HelloWorldSalutation constructor. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory */ public function __construct(ConfigFactoryInterface $config_factory) { $this->configFactory = $config_factory; }
There's nothing too complicated going on here. We added a constructor and set the config factory service on a property. We can now use it to load our configuration object that we saved in the form. However, before we do that, we should also use the ConfigFactoryInterface
class at the top of the file:
use Drupal\Core\Config\ConfigFactoryInterface;
Now, at the top of the getSalutation()
method, we can add the following bit:
$config = $this->configFactory->get('hello_world.custom_salutation'); $salutation = $config->get('salutation'); if ($salutation !== "" && $salutation) { return $salutation; }
With this addition, we are loading the configuration object we saved in the form, and from it, we request the salutation
key, where, if you remember, we stored our message. If there is a value in there, we will return it. Otherwise, the code will continue, and our previous logic of time-based greeting will apply.
So now, if we reload our initial page, the message we saved through the form should show up. If we then return to the form and remove the message, this page should default back to the original dynamic greeting. Neat, right?
Let's now take a look at how we can create a custom block that we can place anywhere we like and that will output the same thing as our page.