Categories

Building Drupal 8 modules: a practical guide

05.07.2016
Building Drupal 8 modules: a practical guide
Author:

Let’s delve into Drupal module development!
Here is a practical guide by InternetDevels developer
on how to create modules for Drupal 8.

In this article, we will look at the process of building modules for Drupal 8. Specifically, we will create two pages, one of which will add data to its own table, and the other will display the content the right way for us. So let’s begin!

Creating a Drupal 8 module: a step-by-step-guide

Setting up the environment

Before you build your Drupal 8 module, the first thing I would suggest to do is to set up the environment for easy coding. I use phpstorm — here’s the link to set it up: https://confluence.jetbrains.com/display/PhpStorm/Drupal+Development+using+PhpStorm

Creating a directory for your Drupal 8 module

After setting up the environment, you need to create a folder for the module at this path /modules/custom/mypage (in Drupal 7, the modules are here: /sites/all/modules).

Building an .info file

Next, we need to come up with the name for our Drupal 8 module and create a file to describe the module. I called it mypage, so let’s call the file mypage.info.yml (in Drupal 7, it used to be mypage.info).

Here's what I got:

# Module name.
name: My Page
 
# Module detailed description.
description: Create page in drupal 8
 
# Specify in which module group the module will be displayed on page /admin/modules.
package: Custom
 
# Specify that it is a module. It can be (module, theme, profile).
type: module
 
# Key that defines the core version. Required!!!
core: 8.x
 
# Here specify the name of the route (which is declared in mypage.routing.yml file) for module configuration.
configure: mypage.add_record
 
# There also can be these values:
# dependencies - the list of modules our module depends on.
# test_dependencies - the list of modules that will be enabled when running automated tests.
# hidden: TRUE - the option hides this module from the list of modules.
# To show all hidden modules in the list, you should specify $settings['extension_discovery_scan_tests'] = TRUE в settings.php.

You can find a detailed description of all these properties here: https://www.drupal.org/node/2000204

Creating a .module file

In Drupal 7, the .module file was required, and in Drupal 8, it is optional. In this file, I created my theme so the data is displayed the right way.

 <?php
 
/**
* Implements hook_theme().
*/
function mypage_theme() {
 return array(
   'mypage_theme' => array( // Theme name
 	'variables' => array( // Array of name for varialbles
   	'data' => array(),
 	),
 	'templates' => 'templates/mypage-theme', // Path to the template without .html.twig in which the same variables will be available as specified above
   ),
 );
}

In Drupal 8, unlike Drupal 7, you cannot create a theme that will render html — everything is in the TWIG template.

Creating a TWIG template

In Drupal 8, the template engine has changed. Instead of the usual PHPtemplate, we have TWIG. Check out all the differences and working instructions here: https://www.drupal.org/theme-guide/8/twig

In our theme, we specified the templates/mypage-theme' route, so, in the module root folder, make a templates folder and create a mypage-theme.html.twig in it. Here’s where our HTML will be located.

 <div class="row column text-center">
 <h2>{{ data.title }}</h2>
 <hr>
 <p>{{ data.body }}</p>
</div>

Here you can see how the data variable that we specified in the hook_theme() works.

Creating a src directory

Next, we need to create a subdirectory in our module folder, in which we will store controllers, plug-ins, forms, templates and tests. This subdirectory must be called src. This will let the controller class be added to the startup (PSR-4 http://www.php-fig.org/psr/psr-4/) automatically, so there is no need to enable anything manually.

Creating a table in the database

Creating tables has not changed in any way — we still have hook_shema() in Drupal 8. All types of tables can be found here https://www.drupal.org/node/159605

Actually, we will create a mypage.install file in the module root, which is responsible for the actions during the installation or update. For the test, I created a table with 3 fields where I will write the data from the configuration form (more about that later).

 <?php

/**
* @file
* Install, update and uninstall functions for the mypage module.
*/

/**
* Implements hook_schema().
*/
function mypage_schema() {
 $schema['mypage'] = array(
   'description' => 'Custom mypage table.',
   'fields' => array(
     'id' => array(
       'type' => 'serial',
       'unsigned' => TRUE,
       'not null' => TRUE,
     ),
     'title' => array(
       'description' => 'Title page',
       'type' => 'varchar',
       'length' => 255,
       'not null' => TRUE,
       'default' => '',
     ),
     'body' => array(
       'description' => 'Body page',
       'type' => 'text',
       'not null' => TRUE,
       'size' => 'big',
     ),
   ),
   'primary key' => array('id'),
 );
 return $schema;

Creating a service

In earlier Drupal versions, during page rendering, all the functions and methods were initiated, regardless of whether we used them or not. But this affects the speed of the application, so in Drupal 8 we got a useful thing called services. It was borrowed from Symphony, and allows us to name the functions we use when writing code.

To create a service, we need to create a file named mypage.services.yml, in which we will describe the service itself.

services:
 // Service name.
 mypage.db_logic:
   // Class that renders the service.
   // As Drupal 8 uses PSR-4 autoloader, we skip src.
   class: Drupal\mypage\MyPageDbLogic
   // Arguments that will come to the class constructor.
   arguments: ['@database']
   // A more detailed explanation: https://www.drupal.org/node/2239393.
   tags:
 	- { name: backend_overridable }

Find the detailed description here: https://www.drupal.org/node/2133171

This service is necessary for working with the table that we created above. The @database argument lets us work with the database.

Service callback in the code

 $db_logic = \Drupal::service('mypage.db_logic');

A class for the service

 <?php
 
namespace Drupal\mypage;
 
use Drupal\Core\Database\Connection;
 
/**
* Defines a storage handler class that handles the node grants system.
*
* This is used to build node query access.
*
* @ingroup mypage
*/
class MyPageDbLogic {
 
 /**
  * The database connection.
  *
  * @var \Drupal\Core\Database\Connection
  */
 protected $database;
 
 /**
  * Constructs a MyPageDbLogic object.
  *
  * @param \Drupal\Core\Database\Connection $database
  *   The database connection.
  */
 // The $database variable came to us from the service argument.
 public function __construct(Connection $database) {
   $this->database = $database;
 }
 
 /**
  * Add new record in table mypage.
  */
 public function add($title, $body) {
   if (empty($title) || empty($body)) {
 	return FALSE;
   }
   // Example of working with DB in Drupal 8.
   $query = $this->database->insert('mypage');
   $query->fields(array(
 	'title' => $title,
 	'body' => $body,
   ));
   return $query->execute();
 }
 
 /**
  * Get all records from table mypage.
  */
 public function getAll() {
   return $this->getById();
 }
 
 /**
  * Get records by id from table mypage.
  */
 public function getById($id = NULL, $reset = FALSE) {
   $query = $this->database->select('mypage');
   $query->fields('mypage', array('id', 'title', 'body'));
   if ($id) {
 	$query->condition('id', $id);
   }
   $result = $query->execute()->fetchAll();
   if (count($result)) {
 	if ($reset) {
   	$result = reset($result);
 	}
 	return $result;
   }
   return FALSE;
 }
 
}

Creating a routing

In Drupal 8, the hook_menu is replaced with routings, and it is all presented in one file named mypage.routing.yml. See more details here: https://www.drupal.org/node/2186285

 // Routing name, it is used for generating links, redirects and more.
mypage.add_record:
 // The path that will be on the sits.
 path: '/admin/mypage/add_record'
 defaults:
 // Page title
   _title: 'Add record'
   // Display on the form page. Analog of drupal_get_form
   _form: '\Drupal\mypage\Form\ConfigFormMyPage'
 requirements:
 // Permissions
   _permission: 'access simple page'
 
mypage.view:
 path: '/mypage/{mypage_id}'
 defaults:
   _title: 'My page'
   // page callback. Method that renders your page
   _controller: '\Drupal\mypage\Controller\MyPageController::content'
 requirements:
   _permission: 'view content'
   // You can use regular expressions for correct argument content.
   // In this case, only integers will be available in the page_id the variable, otherwise 404.
   mypage_id: \d+

Creating a controller

First, some theory on MVC:

Model provides certain information (data and methods of working with this data) and responds to queries, changing its state. It does not contain information on how this information can be visualized.

View is responsible for the information display (visualization). A form (a window) with graphic elements often plays this role.

Controller provides communication between the user and the system: it controls the input of information by the user, and utilizes the Model and the View for the necessary reaction.

Create a file on the /modules/mypage/src/Controller path and call it MyPageController.php

 <?php
/**
* @file
* Contains \Drupal\mypage\Controller\MyPageController.
*/
 
namespace Drupal\mypage\Controller;
 
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
class MyPageController extends ControllerBase {
 // The name of the variable is the same as in the route!!!
 public function content($mypage_id = NULL) {
   // Service loading.
   $db_logic = \Drupal::service('mypage.db_logic');
   if ($record = $db_logic->getById($mypage_id, TRUE)) {
 	return array(
   	// Working with our theme.
   	'#theme' => 'mypage_theme',
   	'#data' => $record,
 	);
   }
   // Will render: page not found.
   throw new NotFoundHttpException();
 }
}

 

 Working with configs

Instead of variable_set/get, we now have config. To make a variable, create a configform_mypage.schema.yml in /mypage/config/schema and specify:

 // Назва конфіга
configform_mypage.settings:
 type: config_object
 label: 'Configform Example settings'
 mapping:
   email_address:
     type: string
     label: 'This is the example email address.'

See more details here: https://www.drupal.org/node/1905070

Example:

 $config = $this->config('configform_mypage.settings');
$config->set('email_address', ‘test’);
$config->save();

Creating Config Form

Creating forms in Drupal 8 has changed dramatically — instead of the usual hooks, we now have classes. So let’s create a ConfigFormMyPage.php file and place it here: /modules/mypage/src/Form

 <?php
 
/**
* @file
* Contains \Drupal\mypage\Form\ConfigFormMyPage.
*/
 
namespace Drupal\mypage\Form;
 
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\Component\Utility\SafeMarkup;
 
class ConfigFormMyPage extends ConfigFormBase {
 
 /**
  * {@inheritdoc}.
  */
 // Method that renders form id.
 public function getFormId() {
   return 'configform_mypage_form';
 }
 
 /**
  * {@inheritdoc}.
  */
 // Instead of hook_form.
 public function buildForm(array $form, FormStateInterface $form_state) {
   $form = parent::buildForm($form, $form_state);
   $config = $this->config('configform_mypage.settings');
   $form['email'] = array(
 	'#type' => 'email',
 	'#title' => $this->t('Your .com email address.'),
 	'#default_value' => $config->get('email_address'),
   );
   $form['title'] = array(
 	'#type' => 'textfield',
 	'#title' => $this->t('Title'),
 	'#required' => TRUE,
   );
   $form['body'] = array(
 	'#type' => 'textarea',
 	'#title' => $this->t('Body'),
 	'#rows' => 5,
 	'#required' => TRUE,
   );
   $db_logic = \Drupal::service('mypage.db_logic');
   $data = $db_logic->getAll();
   if ($data) {
 	$form['data'] = array(
   	'#type' => 'table',
   	'#caption' => $this->t('Table Data'),
   	'#header' => array($this->t('id'), $this->t('Title'), $this->t('Body')),
 	);
 	foreach ($data as $item) {
   	// Example of link creation.
   	// The first argument is route name, the second argument are its parameters
   	$url = Url::fromRoute('mypage.view', array(
     	'mypage_id' => $item->id,
   	));
   	$form['data'][] = array(
     	'id' => array(
       	'#type' => 'markup',
       	'#markup' => \Drupal::l($item->id, $url),
     	),
     	'title' => array(
       	'#type' => 'markup',
       	'#markup' => $item->title,
     	),
     	'body' => array(
       	'#type' => 'markup',
       	'#markup' => $item->body,
     	),
   	);
 	}
   }
   return $form;
 }
 
 /**
  * {@inheritdoc}
  */
 // Instead of hook_form_validate.
 public function validateForm(array &$form, FormStateInterface $form_state) {
   if (strpos($form_state->getValue('email'), '.com') === FALSE) {
 	$form_state->setErrorByName('email', $this->t('This is not a .com email address.'));
   }
 }
 
 /**
  * {@inheritdoc}
  */
 // Instead of hook_form_submit.
 public function submitForm(array &$form, FormStateInterface $form_state) {
   $db_logic = \Drupal::service('mypage.db_logic');
   $title = SafeMarkup::checkPlain($form_state->getValue('title'));
   $body = SafeMarkup::checkPlain($form_state->getValue('body'));
 
   $db_logic->add($title, $body);
   // Instead of variable_set/get we have config.
   // Example of working with them.
   $config = $this->config('configform_mypage.settings');
   $config->set('email_address', $form_state->getValue('email'));
   $config->save();
   return parent::submitForm($form, $form_state);
 }
 
 /**
  * {@inheritdoc}
  */
 // An array of names of configuration objects that are available for editing.
 protected function getEditableConfigNames() {
   return ['configform_mypage.settings'];
 }
 
}

Adding a link to the menu item

Let’s add a link to the admin menu — we will need to create a mypage.links.menu.yml file in the module root and specify the following:

mypage.view:
 // Title
 title: 'Add content for mypage'
 // Menu system name
 parent: system.admin
 // Route name.
 route_name: mypage.add_record

See more details here https://www.drupal.org/developing/api/8/menu

As a result, the file structure should look like:

Congrats, you have created your Drupal 8 module!

See more blog posts by our developers on Drupal 8:

Theming in Drupal 8: tips and examples for developers

Creating modal windows (pop-ups) in Drupal 8: full tutorial

Using Twig in Drupal 8

Configuration in Drupal 8: tips and examples for developers

Drupal 8 development: useful tips for devs

10 votes, Rating: 5

Read also

1

"Drupalgeddon", online shops in Drupal 8, the future of hooks and much more...We promise that our today’s conversation with the famous drupaler Andypost will be interesting for developers,...

2

Check out more tips about Drupal 8 by our experienced ...

3

Our Drupal developers will continue sharing Drupal 8 tips...

5

Let's keep getting the most useful web...

Subscribe to our blog updates