GrabDuck

My first Drupal 8 module: step-by-step example | Drupalwoo

:

Our custom module: contact form

For my first Drupal 8 module, I'm going to create a custom 'Contact me' form.  Not that it makes much sense considering that there is already a site-wide contact form shipping with Drupal core, but it's just to see how I can accomplish that in Drupal 8.  Learning by doing is the way to go here, so download the latest version of Drupal 8 and play along!

(Updated as of March 26, 2016 with help from Thomas Van Cleemput, https://github.com/ThVanC/bd_contact_tutorial)

Getting Drupal to find and recognize our module

Custom module folder structure

In Drupal 8, the core and non-core modules are organized a little differently.  The Drupal 8 root folder has a sub-folder called /core where all the core modules reside.  Separately, it has a /modules sub-folder where we can put our custom and contrib modules.

Here's a visual representation

/drupal

/core
...

/modules  -- this is where all the core modules are located, such as block, comment, etc

/profiles

/scripts

/tests

/themes

...

/modules  -- I create these two sub-folders myself to keep the contrib community modules and the ones I write myself separately

/contrib

/custom  -- this is where we will place our new custom module

So let's go ahead and create our module sub-folder under /modules/custom.  Since my module will be called 'BD Contact', we can name our new module folder 'bd_contact'.

.info file is now .info.yml

The .yml (pronounced 'yaml') files require a different syntax to be parsed properly, so our .info files, which are now .info.yml files, will have colons instead of equals signs.

bd_contact.info.yml

name: BD Contact
description: 'BD Custom Contact Form.'
type: module
core: 8.x
package: BD Custom

You can see some much more complicated examples of the new .info.yml file syntax on drupal.org: https://drupal.org/node/1935708

As soon as you've done this, your new custom module should pop up in the 'Extend' tab at /admin/modules.  In Drupal 8, the 'Extend' section replaces the 'Modules' section of Drupal 7:

Enabling the custom module in Drupal 8

Now, don't check and enable it quite yet, since we haven't written the .install file.  Enabling it without that crucial file will not create all the necessary database tables we need to store the data from our contact form.  Read on before enabling!

 

 

Installing our module

The .install file is same as always.  Since our form will only have two fields: name and message, our new custom table will just have three fields: the name and message and an id field which will be an auto-incrementing primary key field:

bd_contact.install

<?php

function bd_contact_schema() {
  $schema['bd_contact'] = array(
    'fields' => array(
      'id'=>array(
        'type'=>'serial',
        'not null' => TRUE,
      ),
      'name'=>array(
        'type' => 'varchar',
        'length' => 40,
        'not null' => TRUE,
      ),
      'message'=>array(
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
      ),
    ),
    'primary key' => array('id'),
  );

  return $schema;

}

 

Now, if the module is installed, our new table will be created! 

Here is what I see if I inspect my database with phpMyAdmin - our brand new bd_custom table with the three fields specified above:

Creating the new module menus and URLs

The routing.yml file

We'll have three distinct pages for our custom BD Contact module:

  • A page which lists all the submissions received so far
  • A page which users will go to in order to create a new submission (i.e. the URL to access the contact form)
  • A page (really more just a URL) to delete submissions

Let's create all of these with the new routing.yml way of doing things.  (Since I just give an example of how to create new routes here, rather than explaining the new routing system in Drupal 8, the least I can do is provide a place that does explain it.  A very useful tutorial is this one on using Drupal 8's new route controllers).

bd_contact.routing.yml

bd_contact_list:
  path: '/admin/content/bd_contact'
  defaults:
    _controller: '\Drupal\bd_contact\Controller\AdminController::content'
  requirements:
    _permission: 'manage bd contact forms'

bd_contact_add:
  path: '/admin/content/bd_contact/add'
  defaults:
    _form: '\Drupal\bd_contact\AddForm'
    _title: 'Create contact'
  requirements:
    _permission: 'use bd contact form'

bd_contact_edit:
  path: 'admin/content/bd_contact/edit/{id}'
  defaults:
    _form: '\Drupal\bd_contact\AddForm'
    _title: 'Edit contact'
  requirements:
    _permission: 'use bd contact form'

bd_contact_delete:
  path: '/admin/content/bd_contact/delete/{id}'
  defaults:
    _form: 'Drupal\bd_contact\DeleteForm'
    _title: 'Delete contact'
  requirements:
    _permission: 'manage bd contact forms'

p.s. We'll implement the permissions below in our .module file, and the AdminController content() method later!

 

hook_menu() in the .module file

Now let's implement hook_menu() in our new .module file, just like we used to, except that we'll include a reference to the route name above that each menu item applies to

bd_contact.module

<?php


/**
* Implements hook_menu()
*/
function bd_contact_array() {
  return array(
    'admin/content/bd_contact' => array(
      'title' => 'BD Contact submissions',
      'route_name' => 'bd_contact_list',
    ),
    'admin/content/bd_contact/add' => array(
      'title' => 'BD Contact',
      'route_name' => 'bd_contact_add',
    ),
    'admin/content/bd_contact/delete/%' => array(
      'title' => 'Delete BD Contact submission',
      'route_name' => 'bd_contact_delete',
    ),
  );
}

/**
 * Implements hook_permission()
 */
/*function bd_contact_permission() {
  return array(
    'manage bd contact forms' => array(
      'title' => t('Manage bd contact form submissions'),
    ),
    'use bd contact form' => array(
      'title' => t('Use the bd contact form'),
    ),
  );
}*/

 

Creating a tab in the /content admin page

The above is perfectly fine to get us started with using our custom module, since the URLs have been created and work well.  But I want to make things just a tad more complicated and create our very own special tab on the /content admin page, just the way the file and comment modules have.

Sooooo, let's create just one more file, this one with the extension .links.task.yml:

bd_contact.links.tasks.yml

bd_contact_list:
  title: BD Contact
  route_name: bd_contact_list
  base_route: system.admin_content

See how this file, in the by now familiar yml format, specifies that the system.admin_content page will be the base_route of this particular page.  That's all that was needed!  Here is our brand new tab:

Menu tab in Drupal 8

 

Implementing Forms the OOP way

PSR-4 compliant folder structure

Drupal 8 core now ships with Symfony2 ClassLoader, and in order for all the php files your module uses to be loaded and managed automatically, they need to keep to the PSR-4 naming conventions.  Once again, since this is just an example of how to make this work, rather than a deep discussion into how the auto loading works and all the benefits of going in this direction, I'm leaving you with two places where you can investigate these topics yourself: https://www.drupal.org/node/2156625 and http://www.sitepoint.com/autoloading-and-the-psr-0-standard/ (good, clear explanation on it, even though it talks about the PSR-0, not PSR-4 standard). Now back to the example.

In order to create all of the php classes and files (we will now create a separate file for each separate class, unlike with Drupal 7) that will govern the functionality of our list, add and delete pages, we need to create a folder structure that can be traversed and loaded by PSR-4 automatically.  It will be expected that the php classes will be in the src directory inside of the module's root directory.  The namespace for each module starts with Drupal\<module name>.  You can also use the Console module to auto-create the necessary PSR-4 compliant folder structure and common files (.info, .module), creating classes with appropriate namespaces, registering routes in YML files, etc.

So let's go ahead and create the following sub-folders in our /bd_contact module directory (I'll discuss the actual php files in a minute):

  • src/
    • Controller/
      • AdminController.php → class Drupal\bd_contact\Controller\AdminController
    • AddForm.php → class Drupal\bd_contact\AddForm
    • BdContactStorage.php → class Drupal\bd_contact\BdContactStorage
    • DeleteForm.php → class Drupal\bd_contact\DeleteForm

Form managing php classes

For this part, the tutorial Forms, OOP style by effulgentsia really helped me put things together, and I'm totally borrowing much of the class and function structure from there, even if I have adjusted and extended the actual implementations.  Check it out for additional insight into all the changes in Drupal 8!

So anyway, now that we have our folder structure in a way that will be recognizable to Drupal 8's auto-loading system, let's create our necessary php classes.  Unlike the way things were frequently implemented in Drupal 7, here we will have lovely, separate, clear php files,with each class being in a separate file.  We will also take advantage of a lot of form implementations available in Drupal core, by using inheritance and so some of our code will be very simple and straight-forward.

 

/src/BdContactStorage.php

<?php

namespace Drupal\bd_contact;

class BdContactStorage {

  static function getAll() {
    $result = db_query('SELECT * FROM {bd_contact}')->fetchAllAssoc('id');
	return $result;
  }

  static function exists($id) {
    $result = db_query('SELECT 1 FROM {bd_contact} WHERE id = :id', array(':id' => $id))->fetchField();
    return (bool) $result;
  }

  static function add($name, $message) {
    db_insert('bd_contact')->fields(array(
	'name' => $name,
	'message' => $message,	
	))->execute();
  }

  static function delete($id) {
    db_delete('bd_contact')->condition('id', $id)->execute();
  }

}

The above is our storage class, used to insert form data into the database, as well as retrieve and delete it.  This is a helper class that will be used internally.  Having this information contained this way would make it easy to change up the way we store the data in the future, if we needed to!

 

/src/AddForm.php

<?php

namespace Drupal\bd_contact;

use Drupal\Core\Form\FormInterface;

class AddForm implements FormInterface {

  function getFormID() {
    return 'bd_contact_add';
  }

  function buildForm(array $form, array &$form_state) {
    $form['name'] = array(
      '#type' => 'textfield',
      '#title' => t('Name'),
    );
    $form['message'] = array(
      '#type' => 'textarea',
      '#title' => t('Message'),
    );
    $form['actions'] = array('#type' => 'actions');
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Add'),
    );
    return $form;
  }

  function validateForm(array &$form, array &$form_state) {
    /*Nothing to validate on this form*/
  }

  function submitForm(array &$form, array &$form_state) {
    $name = $form_state['values']['name'];
    $message = $form_state['values']['message'];
    BdContactStorage::add(check_plain($name), check_plain($message));
    
    watchdog('bd_contact', 'BD Contact message from %name has been submitted.', array('%name' => $name));
    drupal_set_message(t('Your message has been submitted'));
    $form_state['redirect'] = 'admin/content/bd_contact';
    return;
  }

}

 

You'll see above, in the AddForm.php class, the full effects of OOP and inheritance.  The buildForm() method will look very familiar to you from Drupal 6, and the validateForm() and submitForm() functions are called in the correct order, automatically for you as users interact with your form.  Beautiful!  As you can see, in the submitForm() method above, I'm calling the add() method of my storage class I just implemented previously...

Here is what our contact form should look like:

BD Contact Add Message page

 

/src/DeleteForm.php

<?php

namespace Drupal\bd_contact;

use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Url;

class DeleteForm extends ConfirmFormBase {

  protected $id;

  function getFormID() {
    return 'bd_contact_delete';
  }

  function getQuestion() {
    return t('Are you sure you want to delete submission %id?', array('%id' => $this->id));
  }

  function getConfirmText() {
    return t('Delete');
  }

  function getCancelRoute() {
    return new Url('bd_contact.list');
  }

  function buildForm(array $form, array &$form_state, $id = '') {
    $this->id = $id;
    return parent::buildForm($form, $form_state);
  }

  function submitForm(array &$form, array &$form_state) {
    BdContactStorage::delete($this->id);
    watchdog('bd_contact', 'Deleted BD Contact Submission with id %id.', array('%id' => $this->id));
    drupal_set_message(t('BD Contact submission %id has been deleted.', array('%id' => $this->id)));
    $form_state['redirect'] = 'admin/content/bd_contact';
  }
}

The above is also a very straightforward class, which inherits from the base class ConfirmFormBase.  You have to provide some easy details, as you can see, such as what the confirmation question is, where the user should be taken if they cancel out of the process (getCancelRoute()), etc.  The flow of how and when all these methods will be called is all done for you, as long as you implement these simple methods.  We'll see it all in action just as soon as we implement our final class:

 

/src/Controller/AdminController.php

<?php

namespace Drupal\bd_contact;

use Drupal\bd_contact\BdContactStorage;

class AdminController {

  function content() {

    $add_link = '<p>' . l(t('New message'), 'admin/content/bd_contact/add') . '</p>';
        
    // Table header
    $header = array(
      'id' => t('Id'),
      'name' => t('Submitter name'),
      'message' => t('Message'),
      'operations' => t('Delete'),
    );
    
    $rows = array();
    
    foreach(BdContactStorage::getAll() as $id=>$content) {
      // Row with attributes on the row and some of its cells.
      $rows[] = array(
        'data' => array($id, $content->name, $content->message, l('Delete', "admin/content/bd_contact/delete/$id"))
      );
    }

    $table = array(
      '#type' => 'table',
      '#header' => $header,
      '#rows' => $rows,
      '#attributes' => array(
        'id' => 'bd-contact-table',
      ),
    );
    
    return $add_link . drupal_render($table);
  }
}

This class returns the content for the /bd_contact general admin page, which has only a single content() function.  As you can see, it outputs two things:

  1. A 'New message' link at the top
  2. A themed table, whose rows are retrieved by using the BdContactStorage::getAll() method and filled into the table's rows.

If you'll remember (or look back), the route we created for the 'admin/content/bd_contact' page in bd_contact.routing.yml calls this class's content() function to return the contents of the forms that have been submitted so far. 

Here is the output of that main list page after I've submitted two messages:

BD Contact Admin page

 

I hope this was helpful in getting you started in Drupal 8!  I know the example may be a little simplistic and could be better in many ways, but it works and illustrates a lot of the new ways of doing things.  Make sure you see my note below under 'Resources' on the blog post that got me started with this example, as well as all the other places I've read and learned to keep up with all the Drupal 8 initiatives!

Resources

The tutorial Forms, OOP style by effulgentsia really helped me get started!

And here are the remainder of the tutorials listed in this blog post