Porting scanner module to D8 - Part 1

Porting scanner module to D8 - Part 1

Submitted by Christian Crawford on Mon, 05/06/2019 - 11:35

This is the first part in a series of posts that I have written on this topic. In this first post I will discuss the reasons why I ported the module to Drupal 8 and some of the more basic module code (YAML files and forms). In the next post I will go over the Plugin system, how to write a custom Plugin, and how to implement the Batch API.

I recently found myself needing to the port the scanner module to Drupal 8 after being unable to find a module which met my requirements. In this article I will be going over the code itself rather than philosophical software development principles in my previous post. When I first embarked on this endeavor, I estimated that it would take me a three or four days to get it to feature parity with the Drupal 7 version since it seemed, concept wise, simple enough. This estimation was make before I had looked at any of the existing code. When all was said and done however, I had put in roughly 105 hours. I will now go over the various "components" of the module and how they were transformed into D8 code.

I gave a presentation at our local Drupal meetup and the video can be view here.


If you have ever written a module in D7 you've more than likely written a hook_menu implementation. Transforming that into D8 code is pretty straight forward. Instead of adding a hook_menu function to our module file we create a mymod.routing.yml and create entries for each of the corresponding values in the $items array.

  path: 'admin/content/scanner'
    _title: 'Search and Replace Scanner'
    _form: '\Drupal\scanner\Form\ScannerForm'
    _permission: 'administer nodes'
  path: 'admin/config/content/scanner'
    _title: 'Search and Replace Scanner'
    _form: '\Drupal\scanner\Form\ScannerAdminForm'
    _permission: 'administer scanner settings'
  path: 'admin/content/scanner/undo/{id}/confirm'
    _title: 'Confirm Undo'
    _form: '\Drupal\scanner\Form\ScannerConfirmUndoForm'
    _permission: 'perform search and replace'
    id: '\d+'

Here is an example of the main routes for the module. In the definition we have the route name, path, defaults, and requirements. There are a few key points to understand in these route definitions.

  1. path
    • This will be the url that whatever logic you've will be executed on.
  2. defaults
    • This is where you provide default properties of your route
    • The most common values are "_controller" and "_form"
    • _form:
      • Whatever form you specific is the one that will be rendered on this route.
      • The value must be the fully qualified path. 
  3. requirements:
    • Allows you to restrict access to your routes in several forms. The most common properties are: _permission, _role, or _access
    • In each of our routes we are restricting access with the a specified permission
    • If your route contains a parameter you can specify a regular express to match it against, which we're using in the third route to only match number values for the id parameter

Similar to routes, menu links previously were created in hook_menu by using the "type" key in your $items definitions. In D8 they have been broken out into their own YAML files just like routes. If all you need is a menu link then you can create mymod.links.menu.yml like this one and call it a day.

  title: Search and Replace Scanner
  route_name: scanner.admin_content
  parent: system.admin_content
  description: 'Perform search and replace on chosen text fields.'
  title: 'Search and Replace Scanner'
  parent: system.admin_config_content
  description: 'Configure defaults and what fields can be searched and replaced.'
  route_name: scanner.admin_config

The title is the text that will appear in the menu.
The route_name must match the name you defined in the routing.yml file.
If you want your link to appear in a particular menu then you specify it as the value of the parent key.
The description is what will appear in the admin ui.

In the case of the scanner module not only does it have two menu links, but it also has several local tasks. These are represented as tabs and can be up to two levels deep. The scanner module includes one tab which appears on the /admin/content route and then two sub tabs inside that one.

  title: 'Search and Replace Scanner'
  route_name: scanner.admin_content
  base_route: system.admin_content
  title: 'Scan and Replace'
  route_name: scanner.admin_content
  parent_id: scanner.admin_content
  title: 'Undo'
  route_name: scanner.undo
  parent_id: scanner.admin_content
  weight: 1

The first item is the main "tab". You need to provide: a title, the route_name (same as in menu_links.yml), and the base route (which route this menu should be added on). The next two items are the sub-items, one for the search and replace form and one for the undo form. These entries require one additional key called "parent_id". The value of this should be the name you gave to the parent tab ("scanner.admin_content" in our case).


The scanner module creates three permissions which are used to limit access to certain features. As with the other aspects mentioned above its prior implementation (hook_permission) has been transformed into a YAML (mymod.permissions.yml).

administer scanner settings:
  title: 'Administer scanner settings'
perform search and replace:
  title: 'Perform search and replace'
  description: 'Users with this permission can perform both the search and replace operations.'

Module File

In D8 the module file has become less important since a lot of the functionality has been broken out into yaml files, plugins, services, etc. The scanner module implements a single hook (hook_theme).

 * Implements hook_theme().
function scanner_theme($existing, $type, $theme, $path) {
  return [
    'scanner_results' => [
      'variables' => [
        'data' => NULL,

We need this hook in order to let the theme system know that we are going to use a custom twig template. We simply return an array with the first key being the #theme value of our render array as well as them template name. You can optionally specify default values for your $variables variable. 


The D8 port contains 4 forms which handle each of the scan, replace, undo operations as well as the administration form.

The ScannerForm.php and ScannerAdminForm.php aren't anything special. The other two forms however are interesting, to me at least, because extend a class of which I was unaware of before this endeavor. Both of them [should] require a user to confirm the action before executing it, and therefore they extend the Drupal\Core\Form\ConfirmFormBase class. This class has two additional methods which you must implement: getCancelUrl and getQuestion. Below is a snippet from the undo confirmation form.

class ScannerConfirmUndoForm extends ConfirmFormBase {
  public function getFormId() {
    return 'scanner_confirm_undo_form';

  public function buildForm(array $form, FormStateInterface $form_state, $id = null) {
    $form = parent::buildForm($form, $form_state);
    $form['undo_id'] = [
      '#type' => 'hidden',
      '#value' => $id
    return $form;

  public function submitForm(array &$form, FormStateInterface $form_state) {
   // submit logic here

  public static function batchUndo($data,$undo_id,&$context) {
    // Batch logic here

  public static function batchFinished($success, $results, $operations) {
    // Code here

  public function getCancelUrl() {
    return new Url('scanner.undo');

  public function getQuestion() {
    return $this->t('Are you sure you want to undo this?');

The getCancelUrl method should return a route which the user is redirected to if they click the "cancel" button.
The getQuestion method returns a string which is printed at the top of the confirmation form.

Confirmation form

This topic is continued in part two.

Christian Crawford

Profile picture for user Christian Crawford
Senior Engineering Manager & Lead Software Developer
  • Drupal site building, module development, theming (since Drupal 7)
  • Cloud Infrastructure (AWS, Azure, Google)
  • Docker & Kubernetes
  • SQL (MySQL and Oracle), NoSQL (MongoDB)
  • ReactJS, Svelte, jQuery, NodeJS
  • Nginx and Apache Stacks