Khanh Hoang - Kenn
Kenn is a user experience designer and front end developer who enjoys creating beautiful and usable web and mobile experiences.
In the first installment of this article series on Drupal 8 module development we started with the basics. We’ve seen what files were needed to let Drupal know about our module, how the routing process works and how to create menu links programatically as configuration.
In this tutorial we are going to go a bit further with our sandbox module found in this repository and look at two new important pieces of functionality: blocks and forms. To this end, we will create a custom block that returns some configurable text. After that, we will create a simple form used to print out user submitted values to the screen.
A cool new change to the block API in D8 has been a switch to making blocks more prominent, by making them plugins (a brand new concept). What this means is that they are reusable pieces of functionality (under the hood) as you can now create a block in the UI and reuse it across the site – you are no longer limited to using a block only one time.
Let’s go ahead and create a simple block type that prints to the screen Hello World! by default. All we need to work with is one class file located in the src/Plugin/Block
folder of our module’s root directory. Let’s call our new block type DemoBlock
, and naturally it needs to reside in a file called DemoBlock.php
. Inside this file, we can start with the following:
<?php namespace Drupal\demo\Plugin\Block; use Drupal\block\BlockBase; use Drupal\Core\Session\AccountInterface; /** * Provides a 'Demo' block. * * @Block( * id = "demo_block", * admin_label = @Translation("Demo block"), * ) */ class DemoBlock extends BlockBase { /** * {@inheritdoc} */ public function build() { return array( '#markup' => $this->t('Hello World!'), ); } /** * {@inheritdoc} */ public function access(AccountInterface $account) { return $account->hasPermission('access content'); } }
Like with all other class files we start by namespacing our class. Then we use the BlockBase
class so that we can extend it, as well as the AccountInterface
class so that we can get access to the currently logged in user. Then follows something you definitely have not seen in Drupal 7: annotations.
Annotations are a PHP discovery tool located in the comment block of the same file as the class definition. Using these annotations we let Drupal know that we want to register a new block type (@Block
) with the id of demo_block
and the admin_label
of Demo block (passed through the translation system).
Next, we extend the BlockBase
class into our own DemoBlock
, inside of which we implement two methods (the most common ones you’ll implement). The build()
method is the most important as it returns a renderable array the block will print out. The access()
method controls access rights for viewing this block. The parameter passed to it is an instance of the AccountInterface
class which will be in this case the current user.
Another interesting thing to note is that we are no longer using the t()
function globally for translation but we reference the t()
method implemented in the class parent.
And that’s it, you can clear the caches and go to the Block layout
configuration page. The cool thing is that you have the block types on the right (that you can filter through) and you can place one or more blocks of those types to various regions on the site.
Now that we’ve seen how to create a new block type to use from the UI, let’s tap further into the API and add a configuration form for it. We will make it so that you can edit the block, specify a name in a textfield and then the block will say hello to that name rather than the world.
First, we’ll need to define the form that contains our textfield. So inside our DemoBlock
class we can add a new method called blockForm()
:
/** * {@inheritdoc} */ public function blockForm($form, &$form_state) { $form = parent::blockForm($form, $form_state); $config = $this->getConfiguration(); $form['demo_block_settings'] = array( '#type' => 'textfield', '#title' => $this->t('Who'), '#description' => $this->t('Who do you want to say hello to?'), '#default_value' => isset($config['demo_block_settings']) ? $config['demo_block_settings'] : '', ); return $form; }
This form API implementation should look very familiar from Drupal 7. There are, however, some new things going on here. First, we retrieve the $form
array from the parent class (so we are building on the existing form by adding our own field). Standard OOP stuff. Then, we retrieve and store the configuration for this block. The BlockBase
class defines the getConfiguration()
method that does this for us. And we place the demo_block_settings
value as the #default_value
in case it has been set already.
Next, it’s time for the submit handler of this form that will process the value of our field and store it in the block’s configuration:
/** * {@inheritdoc} */ public function blockSubmit($form, &$form_state) { $this->setConfigurationValue('demo_block_settings', $form_state['values']['demo_block_settings']); }
This method also goes inside the DemoBlock
class and all it does is save the value of the demo_block_settings
field as a new item in the block’s configuration (keyed by the same name for consistency).
Lastly, we need to adapt our build()
method to include the name to say hello to:
/** * {@inheritdoc} */ public function build() { $config = $this->getConfiguration(); if (isset($config['demo_block_settings']) && !empty($config['demo_block_settings'])) { $name = $config['demo_block_settings']; } else { $name = $this->t('to no one'); } return array( '#markup' => $this->t('Hello @name!', array('@name' => $name)), ); }
By now, this should look fairly easy. We are retrieving the block’s configuration and if the value of our field is set, we use it for the printed statement. If not, use use a generic one. You can clear the cache and test it out by editing the block you assigned to a region and add a name to say hello to. One thing to keep in mind is that you are still responsible for sanitizing user input upon printing to the screen. I have not included these steps for brevity.
The last thing we are going to explore in this tutorial is how to create a simple form. Due to space limitations, I will not cover the configuration management aspect of it (storing configuration values submitted through forms). Rather, I will illustrate a simple form definition, the values submitted being simply printed on the screen to show that it works.
In Drupal 8, form definition functions are all grouped together inside a class. So let’s define our simple DemoForm
class inside src/Form/DemoForm.php
:
<?php /** * @file * Contains \Drupal\demo\Form\DemoForm. */ namespace Drupal\demo\Form; use Drupal\Core\Form\FormBase; class DemoForm extends FormBase { /** * {@inheritdoc}. */ public function getFormId() { return 'demo_form'; } /** * {@inheritdoc}. */ public function buildForm(array $form, array &$form_state) { $form['email'] = array( '#type' => 'email', '#title' => $this->t('Your .com email address.') ); $form['show'] = array( '#type' => 'submit', '#value' => $this->t('Submit'), ); return $form; } /** * {@inheritdoc} */ public function validateForm(array &$form, array &$form_state) { if (strpos($form_state['values']['email'], '.com') === FALSE ) { $this->setFormError('email', $form_state, $this->t('This is not a .com email address.')); } } /** * {@inheritdoc} */ public function submitForm(array &$form, array &$form_state) { drupal_set_message($this->t('Your email address is @email', array('@email' => $form_state['values']['email']))); } }
Apart from the OOP side of it, everything should look very familiar to Drupal 7. The Form API has remained pretty much unchanged (except for the addition of some new form elements and this class encapsulation). So what happens above?
First, we namespace the class and use the core FormBase
class so we can extend it with our own DemoForm
class. Then we implement 4 methods, 3 of which should look very familiar. The getFormId()
method is new and mandatory, used simply to return the machine name of the form. The buildForm()
method is again mandatory and it builds up the form. How? Just like you are used to from Drupal 7. The validateForm()
method is optional and its purpose should also be quite clear from D7. And finally, the submitForm()
method does the submission handling. Very logical and organised.
So what are we trying to achieve with this form? We have an email field (a new form element in Drupal 8) we want users to fill out. By default, Drupal checks whether the value input is in fact an email address. But in our validation function we make sure it is a .com
email address and if not, we set a form error on the field. Lastly, the submit handler just prints a message on the page.
One last thing we need to do in order to use this form is provide a route for it. So edit the demo.routing.yml
file and add the following:
demo.form: path: '/demo/form' defaults: _form: '\Drupal\demo\Form\DemoForm' _title: 'Demo Form' requirements: _permission: 'access content'
This should look familiar from the previous article in which we routed a simple page. The only big difference is that instead of _content
under defaults
, we use _form
to specify that the target is a form class. And the value is therefore the class name we just created.
Clear the caches and navigate to demo/form
to see the form and test it out.
If you are familiar with drupal_get_form()
and are wondering how to load a form like we used to in Drupal 7, the answer is in the global Drupal class. Thus to retrieve a form, you can use its formBuilder()
method and do something like this:
$form = \Drupal::formBuilder()->getForm('Drupal\demo\Form\DemoForm');
Then you can return $form
which will be the renderable array of the form.
In this article we’ve continued our exploration of Drupal 8 module development with two new topics: blocks and forms. We’ve seen how to create our own block type we can use to create blocks in the UI. We’ve also learned how to add a custom configuration to it and store the values for later use. On the topic of forms, we’ve seen a simple implementation of the FormBase
class that we used to print out to the screen the value submitted by the user.
In the next tutorial we will take a quick look at configuration forms. We will save the values submitted by the user using the Drupal 8 configuration system. Additionally, we will look at the service container and dependency injection and how those work in Drupal 8. See you then.