You are here

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

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

Share

Comments

Hi. Firstly, thanks for doing this post!

I have just worked through the whole module and updated it to get it to work with drupal 8 as of Jul 9 (downloaded date). Naturally there were some changes needed to get it to work. I will try to contact you to see if you would like updated code.

Hi Randy,

I haven't updated this post in a while, so any suggested edits would be very helpful.  I bet it can cause quite a bit of frustration otherwise!  I promise I'll incorporate them quickly... :-)

I was able to get it working also with a bit of work. I will get the code up on github over the weekend so you can see the code.

I've also added an edit form which I'll include too.

Great!  I just went ahead and updated all the PSR-0 to PSR-4 stuff per Randy's suggestion (including the resource links for further reading).  Would be great to see your addition/extension to the module anyway though!

Here is the link to the github project: https://github.com/nlisgo/bd_contact

The master branch just contains the working code most of which will be covered by your recent update to PSR-4.

The bd_contact_edit branch just took it a step further to allow an edit screen: https://github.com/nlisgo/bd_contact/tree/bd_contact_edit

Hey,

I just got this module working in beta2. I can push it to a new branch of the repository if you like, but I'll need permission to do so.

p.s. I really do have the best readers!  What a helpful community. :-)

There is no hook_menu in D8.

The code would work without bd_contact_menu.

You are already taking care of that stuff in routing.yml

Hi,

First of all, i would like to say thanks for posting this post.

I have downloaded the code and try to enable the module but i am getting the error message like - " Fatal error: Declaration of Drupal\bd_contact\AddForm::buildForm() must be compatible with Drupal\Core\Form\FormInterface::buildForm(array $form, Drupal\Core\Form\FormStateInterface $form_state) in C:\wamp\www\drupal8\modules\custom\bd_contact\src\AddForm.php on line 7"

Can you please help on this?

Regards,
Venkat

This module is not working with drupal-8.0.0-beta15 . It would be very thankful if you could be able to provide me the working module with the latest version. Actually i am new to drupal and could not able to find any dummy form module.

Thanks in advance.

This is correct.
This Tutorial is OUT OF DATE!
Actually the BuildForm interface did change. It's signature isn't longer 2 pointers as type of array, but an array and a FormStateInterface Object.

I just copy your code, but there is no tab appear in my project. please help me or check your code
many thanks

this module is not working correctly. because, there is no tab created in admin content.
So, can you please help on this code.

Thanks..

Finally anyone did it? Please , how? Code please

I updated the code and commited it on Github. I added a readme file which describes most changes.

https://github.com/ThVanC/bd_contact_tutorial

Hey Thomas,

Thanks so much for doing this - this post is outdated at the moment.  I'll have a look at your code and update it, so folks aren't frustrated reading through in the future.  I'll give you kudos in the body of the tutorial too!  Thanks again!

You're welcome! You did the greatest job by thinking out all concepts which can be used in a Drupal module tutorial. It is hard to find clear and advanced tutorials (more than just a helloworld) and I liked this tutorial. That's why I wanted to update this code and help you and others by sharing my code.

Well... I've said it before, and I'll say it again: I have the best blog readers :).

I've updated all the module files with what you put up.  I'll assume, by the way, that you could get it all to work without making any changes to the form management classes I discuss at the end! 

Hi guys,

I used the updated code given in the comments but the tab still isn't showing up for me. The code is exactly the same. The tutorial was a great help bdw thanks! But just wondering is it working for anybody else? Could it be to do with the way I enable the module?

Got it working now, not sure what I was doing wrong but it works! Thank you!

Thanks guys, it was a great help.

if you would like to send an attachment as it could do?
help me please

Just wondering which you considered to be more appealing when it comes to adding AJAX to your form inputs, after my experience doing so, using a custom form module. Would you rather build the module or write multiple hooks to perform your validation? I considered both, but felt it was "cleaner" to keep the RIDICULOUS amount of code I would need to write in the hook separate from everything else I've had to customize in my theme file. Thoughts?

Hi all.first i want to say thanks for this post i use this code and making some changes according to my requirements but when i come to edit text it giving me whote screen what can i do..plz if anybody done this share the code

i"m not getting that BD contact tab in the content.i'm using drupal8.

have you resolved this?

Hi,

I have followed all the steps but I am not able to create a BD Contact tab in Admin Panel.
Please help me.

Thanks

This is deprecated:
AdminController line 43:
$add_link = '<p>' . \Drupal::l(t('New message'), $url) . '</p>';
What should this be?

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.