Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough
Aug 26 2015
Aug 26

Based on my presentations at DrupalCamp London, on Saturday 28th February 2015 and DrupalCamp Bristol, July 4th 2015

Concept of a field

Fields are the data entry points to a web application. Usually, they provide HTML elements and may be responsible for any manipulation of data before it goes into and comes out of the application. The data captured from a single field can be simple or complex.

Assuming we want to create a field for country, all we need is a single HTML element - textfield or select options. An address field, on the other hand, is a collection of discrete data which may include standalone simple fields including a textfield representing postcode (provided by a core module) and a country field (maybe from a custom or contributed module).

Screenshot of simple and complex fields

In the Drupal world, when considering solutions the first phrase you may hear is, “there’s a module for that!”. However, for the task at hand, “you need a module for that!”. We are now going to write a module which provides this custom country field..

To create a new field in Drupal 7, a number of hooks need to be implemented in a custom module and these include the following:

  • hook_field_info() - the field type definition as well as its settings.
  • hook_field_schema() - the database schema for the field structure.
  • hook_field_widget_info() - the widget types to use for the field type.
  • hook_field_formatter_info() - the display of field values.

The task in Drupal 8 is founded on the same principles although the implementation differs. The first thing to remember here is, “there is a class for that!”. A lot of the hard work has been done in Drupal core and all we need to do is extend some classes and override default methods to suit our implementation.

Comparison of Drupal 7 hooks with Drupal 8 plugins

Creating a module

All contrib and custom modules should be placed inside the “modules” folder in your site root. Core-related code is now in “core”. However, it’s also best practice to have “contrib” and “custom” sub-folder in “modules” for clear separation of these types of modules. So we’ll create our “country” folder under modules\custom. What used to go inside *.info file is now in country.yml, so we create that file too and add the following:

name: Country
type: module
description: Defines a simple country field type.
package: Field types
version: VERSION
core: 8.x
dependencies:
  - field

Inside your module directory, you need a “src” subdirectory to keep all your class files. To organise things further, you need a “Plugin” sub-folder in “src”. There are different types of plugins e.g. fields, actions, blocks and menus. So you need to create another folder called “Field” inside Plugin and you’ll end up with a directory structure like src\Plugin\Field

Example directory structure for a Drupal 8 module

Next, we need to define our data type, widget and formatter. These will be in classes with each occupying its own folder again. Let’s take them one by one.

Data Type

The folder is called FieldType, so create it - src\Plugin\Field\FieldType. Create a class, which we shall call “CountryItem”. The file is called CountryItem.php and in it we should have:

class CountryItem {
}

How do we define our field type? With the new plugin system, this requires an annotation1 - something like a special comment block to allow core classes know about our field type. Your code should now look like this:

/**
 * Plugin implementation of the 'country' field type.
 *
 * @FieldType(
 *   id = "country",
 *   label = @Translation("Country"),
 *   description = @Translation("Stores the ISO-2 name of a country."),
 *   category = @Translation("Custom"),
 *   default_widget = "country_default",
 *   default_formatter = "country_default"
 * )
 */
class CountryItem {
}

The information provided in our annotation is quite similar to that provided when implementing hook_field_info() in Drupal 7. Next, at the top of our file, we add a few things like namespaces and import required core classes.

namespace Drupal\country\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;

Then we make our class inherit from core FieldItem class by extending it.

class CountryItem extends FieldItemBase {
}

There are two functions we must implement in our class - schema() and propertyDefinitions(). They’re as follows:

public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return array(
      'columns' => array(
        'value' => array(
          'type' => 'char',
          'length' => static::COUNTRY_ISO2_MAXLENGTH,
          'not null' => FALSE,
        ),
      ),
      'indexes' => array(
        'value' => array('value'),
      ),
    );
  }

Here we define the schema for this field. The column is to be called “value”, and will hold a 2-character string, representing the ISO-2 name of countries. Oh, don’t forget to add the constant for the length in your class:

const COUNTRY_ISO2_MAXLENGTH = 2;
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties['value'] = DataDefinition::create('string')
      ->setLabel(t('Country'));
    return $properties;
  }

However, we need to add two methods to make our life easier. Firstly, we want to know when our field is considered empty which is what hook_field_is_empty() does in Drupal 7. Then, we want to add some validation so that the country code we want to store doesn’t exceed the maximum length we have defined for our schema. When we are through, our class should look like this:

/**
 * @file
 * Contains \Drupal\country\Plugin\field\field_type\CountryItem.
 */

namespace Drupal\country\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;

/**
 * Plugin implementation of the 'country' field type.
 *
 * @FieldType(
 *   id = "country",
 *   label = @Translation("Country"),
 *   description = @Translation("Stores the ISO-2 name of a country."),
 *   category = @Translation("Custom"),
 *   default_widget = "country_default",
 *   default_formatter = "country_default"
 * )
 */
class CountryItem extends FieldItemBase {

  const COUNTRY_ISO2_MAXLENGTH = 2;

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties['value'] = DataDefinition::create('string')
      ->setLabel(t('Country'));
    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return array(
      'columns' => array(
        'value' => array(
          'type' => 'char',
          'length' => static::COUNTRY_ISO2_MAXLENGTH,
          'not null' => FALSE,
        ),
      ),
      'indexes' => array(
        'value' => array('value'),
      ),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function isEmpty() {
    $value = $this->get('value')->getValue();
    return $value === NULL || $value === '';
  }

  /**
   * {@inheritdoc}
   */
  public function getConstraints() {
    $constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
    $constraints = parent::getConstraints();
    $constraints[] = $constraint_manager->create('ComplexData', array(
      'value' => array(
        'Length' => array(
          'max' => static::COUNTRY_ISO2_MAXLENGTH,
          'maxMessage' => t('%name: the country iso-2 code may not be longer than @max characters.', array('%name' => $this->getFieldDefinition()->getLabel(), '@max' => static::COUNTRY_ISO2_MAXLENGTH)),
        )
      ),
    ));
    return $constraints;
  }
}

Data Input

Now we have defined our data type and we want to store the ISO-2 country code. How do we want users to input data? We have two options - select dropdown options and an autocomplete textfield. The select options can be the default widget.

We start by creating a class called CountryDefaultWidget in src\Plugin\Field\FieldWidget\CountryDefaultWidget.php with the following code:

namespace Drupal\country\Plugin\Field\FieldWidget;

use Drupal;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;

class CountryDefaultWidget extends WidgetBase {
}

There’s still an important thing missing from our widget class - annotation of the class as provider of a FieldWidget. Add this just above the class statement;

/**
 * Plugin implementation of the 'country_default' widget.
 *
 * @FieldWidget(
 *   id = "country_default",
 *   label = @Translation("Country select"),
 *   field_types = {
 *     "country"
 *   }
 * )
 */

This is similar to the old array keys for the old hook_widget_info() in Drupal 7. Additional annotation keys may be defined by a hook_field_widget_info_alter() function.

Our CountryDefaultWidget class isn’t complete yet. Widgets handle how fields are displayed in edit forms. The missing method we need to implement will do this for us. Add this formElement() method:

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $countries = \Drupal::service('country_manager')->getList();
    $element['value'] = $element + array(
        '#type' => 'select',
        '#options' => $countries,
        '#empty_value' => '',
        '#default_value' => (isset($items[$delta]->value) && isset($countries[$items[$delta]->value])) ? $items[$delta]->value : NULL,
        '#description' => t('Select a country'),
      );
    return $element;
}

Other modules may alter the form element provided by this function using hook_field_widget_form_alter() or hook_field_widget_WIDGET_TYPE_form_alter().

This default country widget is a simple widget of select options. Drupal core provides a list of all countries in the world as an array of country names keyed by ISO-2 country codes. This is made available as a service for use anywhere in a Drupal project.

Let’s start off with a complete implementation for this. Create src\Plugin\Field\FieldWidget\CountryAutocompleteWidget.php with this code:

namespace Drupal\country\Plugin\Field\FieldWidget;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal;
use Drupal\Core\Form\FormStateInterface;

/**
 * Plugin implementation of the 'country_autocomplete' widget.
 *
 * @FieldWidget(
 *   id = "country_autocomplete",
 *   label = @Translation("Country autocomplete widget"),
 *   field_types = {
 *     "country"
 *   }
 * )
 */
class CountryAutocompleteWidget extends WidgetBase {
}

There’s nothing unusual here at all. We need to implement same defaultSettings() and formElement() methods as for the default widget. Add this to the class:

public static function defaultSettings() {
    return array(
      'size' => '60',
      'autocomplete_route_name' => 'country.autocomplete',
      'placeholder' => '',
    ) + parent::defaultSettings();
  }

We want a textfield that’s wide enough (‘size’ => 60). For the autocomplete_route_name key we provide the name of the route from which our autocomplete functionality will return matching values. We’ll be implementing that shortly. We don’t want anything as the placeholder.

Finally, let’s add our formElement() method:

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $countries = \Drupal::service('country_manager')->getList();
    $element['value'] = $element + array(
      '#type' => 'textfield',
      '#default_value' =>  (isset($items[$delta]->value) && isset($countries[$items[$delta]->value])) ? $countries[$items[$delta]->value] : '',
      '#autocomplete_route_name' => $this->getSetting('autocomplete_route_name'),
      '#autocomplete_route_parameters' => array(),
      '#size' => $this->getSetting('size'),
      '#placeholder' => $this->getSetting('placeholder'),
      '#maxlength' => 255,
      '#element_validate' => array('country_autocomplete_validate'),
    );
    return $element;
  }

This is a standard autocomplete widget. Looking at the FAPI array keys, some are very familiar. #autocomplete_route_name matches what we entered in our defaultSettings() a while ago. The value is retrieved from there with $this->getSetting(‘autocomplete_route_name’). The same goes for #size and #placeholder. Our #autocomplete_route_parameters has no default value. In order to ensure that the final value to be submitted doesn’t include unwanted values, we add #element_validate and enter the name of our callback function. We will also implement this shortly.

Creating a route

Create a YAML configuration file called country.routing.yml in your main module directory with the following:

country.autocomplete:
  path: '/country/autocomplete'
  defaults:
    _controller: '\Drupal\country\Controller\CountryAutocompleteController::autocomplete'
  requirements:
    _permission: 'administer content types'

The key or name of the route is country.autocomplete which is how this route will be referred to anywhere in the application. At least a route should define 3 things: path, code to execute and who can access the path.

  • path: This is the URL for our AJAX autocomplete calls.
  • _controller: This is the code we want to execute when we visit the defined path. It’s written as CLASS::FUNCTION. If our function requires any parameters, this is where they will be specified. We will be creating our controller shortly.
  • _permission: The string representation of the permission for access control

Now we move on to the creation of our controller class. Create a folder called Controller under src. Then add CountryAutocompleteController.php inside it. Add this code:

/**
 * @file
 * Contains \Drupal\country\Controller\CountryAutocompleteController.
 */

namespace Drupal\country\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Component\Utility\Unicode;
use Drupal;

/**
 * Returns autocomplete responses for countries.
 */
class CountryAutocompleteController {

  /**
   * Returns response for the country name autocompletion.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object containing the search string.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   A JSON response containing the autocomplete suggestions for countries.
   */
  public function autocomplete(Request $request) {
    $matches = array();
    $string = $request->query->get('q');
    if ($string) {
      $countries = \Drupal::service('country_manager')->getList();
      foreach ($countries as $iso2 => $country) {
        if (strpos(Unicode::strtolower($country), Unicode::strtolower($string)) !== FALSE) {
          $matches[] = array('value' => $country, 'label' => $country);
        }
      }
    }
    return new JsonResponse($matches);
  }
}

Whatever we type in our autocomplete widget will get passed to our autocomplete method. Then, we simply search for it in the array of country names which we pull from the country_manager service we have come across before. Finally, we return any matches or an empty array in a JSON response.

That looks more like it now and we’re nearly there. If you look back at our formElement() method in CountryAutocompleteWidget.php we specified a validation callback. We are going to do that now in our country.module file. Add this code:

/**
 * @file
 * Defines simple country field type.
 */

use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Form\FormStateInterface;

/**
 * Form element validate handler for country autocomplete element.
 */
function country_autocomplete_validate($element, FormStateInterface $form_state) {
  if ($country = $element['#value']) {
    $countries = \Drupal::service('country_manager')->getList();
    $iso2 = array_search($country, $countries);
    if (!empty($iso2)) {
      $form_state->setValueForElement($element, $iso2);
    }
  }
}

We get our array of countries, and compare the value we want to send to the database with possible values. Now that we’re here, let’s just implement hook_help to give some information about our module. Just add this below the last use statement:

/**
 * Implements hook_help().
 */
function country_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.country':
      $output = '';
      $output .= '<h3>' . t('Country') . '</h3>';
      $output .= '<p>' . t('The Country module defines a simple country field type for the Field module. It provides 2 widgets - select options and autocomplete textfield - for this purpose.  See the <a href="https://capgemini.github.io/drupal/writing-custom-fields-in-drupal-8//!field-help">Field module help page</a> for more information about fields.', array('!field-help' => url('admin/help/field_ui'))) . '</p>';
      return $output;
  }
}

We have now finished our autocomplete widget and learned something about routing. Not bad at all!

Data Output

We have everything ready for creating our field and allowing users to input data. Everything should work. There’s little work left before we can display the output. That’s where the need for a field formatter comes in. Add a new folder: src\Plugin\Field\FieldFormatter and inside it create CountryDefaultFormatter.php. Then add this code.

/**
 * @file
 * Definition of Drupal\country\Plugin\field\formatter\CountryDefaultFormatter.
 */

namespace Drupal\country\Plugin\Field\FieldFormatter;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal;

/**
 * Plugin implementation of the 'country' formatter.
 *
 * @FieldFormatter(
 *   id = "country_default",
 *   module = "country",
 *   label = @Translation("Country"),
 *   field_types = {
 *     "country"
 *   }
 * )
 */
class CountryDefaultFormatter extends FormatterBase {
}

If we don’t do anything now, everything will work except where we expect to see a country name, we will see the ISO-2 which was saved as the value of the field. To display our country name, we need to override the viewElements() method. So let’s do it:

/**
 * {@inheritdoc}
 */
public function viewElements(FieldItemListInterface $items) {
    $elements = array();
    $countries = \Drupal::service('country_manager')->getList();
    foreach ($items as $delta => $item) {
      if (isset($countries[$item->value])) {
        $elements[$delta] = array('#markup' => $countries[$item->value]);
      }
    }
    return $elements;
}

Once again, we get our array of countries, find the country name for our ISO-2 value and return it as markup. Job done. The module we have created is now functional and can now be installed.

Field UI

Let’s take a look at what fields UI offer us with our Country field. You may use an existing content type or start off with a new one. From the list of content types (admin/structure/types) clicking on the Edit operation will take you to the edit form with three tabs at the top: Manage fields, Manage form display, and Manage display which give us access to our field type, field widget and field formatter, respectively.

  • Field type (Manage fields):

Click “Add field”. Select “Country” from the “Add a new field” select dropdown. That dropdown is populated by all @FieldType plugins from all enabled modules. They are grouped according to the “category” key of field plugins. This is optional, and if it isn’t defined for a field, it goes under “General”. If you inspect it, you’ll see that the name is “country_default” which matches the “id” key of our plugin and the “Country” we see there is the “label”.

Enter “Location” as the field name in the “Label” textfield and save it.

If you want to see how the field type settings are reflected in the field UI, go back to the annotation in CountryItem.php. Edit your plugin label, change it to something like “Country - edited” and save it. Then go back to the fields UI page and you’ll see not only the new value in the select dropdown when creating a new field but also under the “Field Type” column for the new Location field. You can revert the change now.

  • Field widget (Manage form display):

On this screen you can see what was defined in the FieldWidget plugin. In the URL you can see the plugin “id” and the “label” is the description.

  • Field formatter (Manage display):

There isn’t much choice here so we just leave the default formatter as is.

You may now take the completed field module for a spin. Finish by adding it to a content type, create a node and view the output. That’s all. We’ve come to the end of looking at how to create custom field modules and in the process learned a few things about Drupal 8. Go make yourself a coffee. You deserve it!

1: See https://www.drupal.org/node/1882526 for an excellent introduction to annotation-based plugins.

Jan 14 2015
Jan 14

Up until Drupal 8 there has been little to encourage well organised code. It now has PSR-4 autoloading so your classes are automatically included. Even though Drupal 8 is just round the corner, a lot of us will still be using Drupal 7 for quite a while, however that doesn’t mean we can’t benefit from this structure in Drupal 7.

This post covers two parts:

  1. Autoloading class files.
  2. Avoiding extra plumbing to hook into your class methods.

You’re probably familiar with drupal_get_form(‘my_example_form’) which then looks for a function my_example_form(). The issue is that your form definition will no longer be in such a function but within a method in a class. To cover both these parts we will be using two modules:

  1. XAutoLoad - Which will autoload our class.
  2. Cool - Which allows us to abstract the usual functions into class methods.

Drupal 8 was originally using PSR-0 which has been deprecated in favour of PSR-4. As a consequence the Cool module uses PSR-0 in its examples although it does support PSR-4. We will create an example module called psr4_form.

The information on autoloading and folder structure for PSR-4 in Drupal 8 states that we should place our form class in psr4_form/src/Form/FormExample.php however the cool module instead loads from a FormControllers folder: psr4_form/src/FormControllers/FormExample.php.

We can get round this by providing our own hook_forms() as laid out in the Cool module:

/**
* Implements hook_forms().
*/
function psr4_form_forms($form_id, $args) {
  $classes = \Drupal\cool\Loader::mapImplementationsAvailable('Form', '\Drupal\cool\Controllers\FormController');
  unset($classes['Drupal\\cool\\BaseForm']);
  unset($classes['Drupal\\cool\\BaseSettingsForm']);
  $forms = array();
  foreach ($classes as $class_name) {
    $forms[$class_name::getId()] = array(
      'callback' => 'cool_default_form_callback',
      'callback arguments' => array($class_name),
    );
  }

  return $forms;
}

If you are ok placing your class in the FormControllers folder then you can omit the above function to keep your .module file simple or you could put the hook in another module. Potentially the Cool module could be updated to reflect this.

This class requires a namespace of the form Drupal\\Form. It also extends the BaseForm class provided by the Cool module so we don’t need to explicitly create our form functions:

namespace Drupal\psr4_form\Form;

class FormExample extends \Drupal\cool\BaseForm {
  ...
}

Within our FormExample class we need a method getId() to expose the form_id to Drupal:

public static function getId() {
  return 'psr4_form';
}

And of course we need the form builder:

public static function build() {
  $form = parent::build();
  $form['my_textfield'] = array(
     '#type' => 'textfield',
     '#title' => t('My textfield'),
   );

   return $form;
}

All that is left is to define your validate and submit methods following the Drupal 8 form API.

At the time of writing, the Cool module isn’t up to date with Drupal 8 Form API conventions. I started this blog post with the intention of a direct copy and paste of the src folder. Unfortunately the methods don’t quite follow the exact same conventions and they also need to be static:

Drupal 7 Drupal 8 getId getFormId build buildForm validate validateForm submit submitForm

This example module can be found at https://github.com/oliverpolden/psr4_form.

Drupal 8 is just round the corner but a lot of us will still be using Drupal 7 for the foreseeable future. Taking this approach allows us to learn and make use of Drupal 8 conventions as well as making it easier to migrate from Drupal 7. It would be nice to see the Cool module be brought up to date with the current API, perhaps something I will be helping with in the not so distant future.

Modules

Information

Jan 07 2015
Jan 07

Drupal 8 is the latest version of Drupal, a modern, PHP 5.4-boasting, REST-capable, object-oriented powerhouse. The concepts are still the same as the previous versions but the approach is now different. Drupal 8 comes with a modern Object Oriented Programming (OOP) approach to most parts of the system thanks to the use of the Symfony2 framework.

I took part in the Drupalcon in Amsterdam and I enjoyed a number of really interesting talks about Drupal 8, among those ‘Drupal 8: The Crash Course’ realized and presented by Larry Garfield. In this post the idea is to recap few key points of his talk as I think they are important to fully understand the basics of this new Drupal version. In case you are interested you can also watch the full talk.

How do I define a module?

In Drupal 8 to define a module we need only a YAML (.info.yml) file:

/modules/d8_example_module/d8_example_module.info.yml

name: D8 Test Module
description: D8 Test Module
type: module
core: 8.x
package: Custom

In Drupal 8 the .module file is not required anymore, so with only the .info.yml file the module is ready to be enabled.

How do I make a page?

Start creating a controller extending the ControllerBase class and return the output of the page:

/modules/d8_example_module/src/Controller/D8ExampleModuleController.php

namespace Drupal\d8_example_module\Controller;

use Drupal\Core\Controller\ControllerBase;

class D8ExampleModuleController extends ControllerBase {

  public function test_page($from, $to) {
    $message = $this->t('%from to %to', [
      '%from' => $from,
      '%to' => $to,
    ]);

    return ['#markup' => $message];
  }
}

Once this is done, within the .routing.yml file we can define the path, the controller, the title and the permissions:

/modules/d8_example_module/d8_example_module.routing.yml

d8_example_module.test_page:
  path: '/test-page/{from}/{to}'
  defaults:
    _controller: 'Drupal\d8_example_module\Controller\D8ExampleModuleController::test_page'
    _title: 'Test Page!'
  requirements:
    _permission: 'access content'

How do I make content themeable?

We still have the hook_theme() function to define our theme:

/modules/d8_example_module/d8_example_module.module

/**
 * Implements hook_theme().
 */
function d8_example_module_theme() {
  $theme['d8_example_module_page_theme'] = [
    'variables' => ['from' => NULL, 'to' => NULL],
    'template' => 'd8-theme-page',
  ];

  return $theme;
}

For the template page Drupal 8 uses Twig, a third-party template language used by many PHP projects. For more info about Twig have a look at Twig in Drupal 8. One of the cool parts of Twig is that we can do string translation directly in the template file:

/modules/d8_example_module/template/d8-theme-page.html.twig

{% trans %}
    {{ from }} to {{ to }}
  {% endtrans %}

And then we assign the theme to the page:

/modules/d8_example_module/src/Controller/D8ExampleModuleController.php

namespace Drupal\d8_example_module\Controller;

use Drupal\Core\Controller\ControllerBase;

class D8ExampleModuleController extends ControllerBase {

  public function test_page($from, $to) {
    return [
      '#theme' => 'd8_example_module_page_theme',
      '#from' => $from,
      '#to' => $to,
    ];
  }
}

How do I define a variable?

Drupal 8 has a whole new configuration system that uses human-readable YAML (.yml) text files to store configuration items. For more info have a look at Managing configuration in Drupal 8.

We define variables in config/install/*.settings.yml:

/modules/d8_example_module/config/install/d8_example_module.settings.yml

default_count: 3

The variables will be stored in the database during the installation of the module. We define the schema for the variables in config/schema/*.settings.yml:

/modules/d8_example_module/config/schema/d8_example_module.settings.yml

d8_example_module.settings:
  type: mapping
  label: 'D8 Example Module settings'
  mapping:
    default_count:
      type: integer
      label: 'Default count'

How do I make a form?

To create a form we extend a ConfigFormBase class:

/modules/d8_example_module/src/Form/TestForm.php

namespace Drupal\d8_example_module\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class TestForm extends ConfigFormBase {
  public function getFormId() {
    return 'test_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('d8_example_module.settings');

    $form['default_count'] = [
      '#type' => 'number',
      '#title' => $this->t('Default count'),
      '#default_value' => $config->get('default_count'),
    ];
    return parent::buildForm($form, $form_state);
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $config = $this->config('d8_example_module.settings');
    $config->set('default_count', $form_state->getValue('default_count'));
    $config->save();
    parent::submitForm($form, $form_state);
  }
}

Then within the .routing.yml file we can define the path, the form, the title and the permissions:

/modules/d8_example_module/d8_example_module.routing.yml

d8_example_module.test_form:
  path: /admin/config/system/test-form
  defaults:
    _form: 'Drupal\d8_example_module\Form\TestForm'
    _title: 'Test Form'
  requirements:
    _permission: 'configure_form'

We use another YAML file (.permissions.yml) to define permissions:

/modules/d8_example_module/d8_example_module.permissions.yml

'configure_form':
  title: 'Access to Test Form'
  description: 'Set the Default Count variable'

We also use another YAML file (.links.menu.yml) to define menu links:

/modules/d8_example_module/d8_example_module.links.menu.yml

d8_example_module.test_form:
  title: 'Test Form'
  description: 'Set the Default Count variable'
  route_name: d8_example_module.test_form
  parent: system.admin_config_system

How do I make a block?

To create a block we extend a ConfigFormBase class:

/modules/d8_example_module/src/Plugin/Block/TestBlock.php

namespace Drupal\d8_example_module\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Test Block.
 *
 * @Block(
 *   id = "test_block",
 *   admin_label = @Translation("Test Block"),
 *   category = @Translation("System")
 * )
 */
class TestBlock extends BlockBase {

  public function build() {
    return [
      '#markup' => $this->t('Block content...'),
    ];
  }
}

In this way the block is ready to be configured in the CMS (/admin/structure/block). Here is an example of a more complex block:

namespace Drupal\d8_example_module\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Test Block.
 *
 * @Block(
 *   id = "test_block",
 *   admin_label = @Translation("Test Block"),
 *   category = @Translation("System")
 * )
 */
class TestBlock extends BlockBase {

  public function defaultConfiguration() {
    return ['enabled' => 1];
  }

  public function blockForm($form, FormStateInterface $form_state) {
    $form['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Configuration enabled'),
      '#default_value' => $this->configuration['enabled'],
    ];

    return $form;
  }

  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['enabled'] = (bool)$form_state->getValue('enabled');
  }

  public function build() {
    if ($this->configuration['enabled']) {
      $message = $this->t('Configuration enabled');
    }
    else {
      $message = $this->t('Configuration disabled');
    }
    return [
      '#markup' => $message,
    ];
  }
}

Structure of a module

The structure of a module should look like the example module d8_example_module:

d8_example_module/
 |
 |- config/
   |
   |- install/
     |
     |- d8_example_module.setting.yaml
     |
     |- schema/
       |
       |- d8_example_module.settings.yaml
 |
 |- src/
   |
   |- Controller/
     |
     |- D8ExampleModuleController.php
   |
   |- Form/
     |
     |- TestForm.php
   |
   |- Plugin/
     |
     |- Block/
       |
       |- TestBlock.php
 |
 |- templates/
   |
   |- d8-theme-page.html.twig
 |
 |- d8_example_module.info.yml
 |
 |- d8_example_module.links.menu.yml
 |
 |- d8_example_module.module
 |
 |- d8_example_module.permissions.yml
 |
 |- d8_example_module.routing.yml

Drupal 8 in 2 steps: Extend a base Class or implement an Interface and tell Drupal about it.

Download the example module

Updates:

  • 19th January 2015
    • The code in this post has been updated to reflect changes in Drupal 8 Beta 4. Thanks to Geoff Lawrence for the updates to the example repo.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web