Basic structure

Last updated on
13 November 2023

loremipsum.info.yml

name: Lorem ipsum
type: module
description: 'Lorem ipsum generator for Drupal'
package: Development
core_version_requirement: ^8 || ^9 || ^10
configure: loremipsum.form

Info files are formatted as YML, and there's a difference between modules and themes that must be made clear via the type declaration. The config declaration points to a route (more on that later) but other than that, there's not much else. In fact, this is the only file you'll strictly need for your module. After saving this (in the root /modules folder, under a new one called loremipsum) you can enable your module in /admin/modules without breaking your website. But, as you'll see further ahead, that's not nearly enough.

loremipsum.module

<?php

use Drupal\Core\Routing\RouteMatchInterface;

/**
 * Implements hook_help().
 */
function loremipsum_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.loremipsum':
      $output = '';
      $output .= '<h2>' . t('Lorem ipsum generator for Drupal.') . '</h2>';
      $output .= '<h3>' . t('Instructions') . '</h3>';
      $output .= '<p>' . t('Lorem ipsum dolor sit amet... <em>Just kidding!</em>') . '</p>';
      $output .= '<p>' . t('If you\'re reading this, you\'ve already installed the module either via <code>composer require drupal/loremipsum</code> or directly downloading it (which <em>should</em> be safe) and have already enabled it in <strong>/admin/modules</strong>.') . '</p>';
      $output .= '<p>' . t('Then, visit <strong>/admin/config/development/loremipsum</strong> and enter your own set of phrases to build random-generated text (or go with the default Lorem ipsum).') . '</p>';
      $output .= '<p>' . t('Lastly, visit <strong>/loremipsum/generate/P/S</strong> where:') . '</p>';
      $output .= '<ul>';
      $output .= '<li>' . t('<strong>P</strong> is the number of <em>paragraphs</em>.') . '</li>';
      $output .= '<li>' . t('<strong>S</strong> is the maximum number of <em>sentences</em>.') . '</li>';
      $output .= '</ul>';
      $output .= '<p>' . t('There is also a generator block in which you can choose how many paragraphs and phrases you want and it\'ll do the rest.') . '</p>';
      $output .= '<p>' . t('If you need, there\'s also a specific <em>generate lorem ipsum</em> permission.') . '</p>';
      $output .= '<h3>' . t('Attention') . '</h3>';
      $output .= '<p>' . t('Most bugs have been ironed out, holes covered, features added. But this module is a work in progress. Please report bugs and suggestions, ok?') . '</p>';
      return $output;
  }
}

It's good practice to put at least a definition of hook_help() here. Also, note the use statement pointing to the RouteMatchInterface class, mentioned in the .info.yml file. As we progress, you'll notice the .module file will also be used to store theming information.

loremipsum.install

<?php

/**
 * @file
 * Installation functions for the Lorem ipsum module.
 */

use Drupal\user\RoleInterface;

/**
 * Implements hook_install().
 */
function loremipsum_install() {
  user_role_change_permissions(RoleInterface::ANONYMOUS_ID, [
      'generate lorem ipsum' => TRUE,
  ]);
}

Here we have the use of another class: RoleInterface. Basically, this file tells Drupal: "once this module is enabled, look for the generate lorem ipsum permission and enable it".

But where is this permission defined?

loremipsum.permissions.yml

generate lorem ipsum:
  title: 'Generate Lorem ipsum'

As you can see, it's very simple. The full syntax is in the PermissionHandler documentation.

loremipsum.routing.yml

loremipsum.generate:
  path: '/loremipsum/generate/{paragraphs}/{phrases}'
  defaults:
    _controller: '\Drupal\loremipsum\Controller\LoremIpsumController::generate'
  requirements:
    _permission: 'generate lorem ipsum'

loremipsum.form:
  path: '/admin/config/development/loremipsum'
  defaults:
    _form: '\Drupal\loremipsum\Form\LoremIpsumForm'
    _title: 'Lorem ipsum settings'
  requirements:
    _permission: 'administer site configuration'

Each entry (without indentation) in the routing file points to a route, with subsequent indented lines detailing specific settings.

The loremipsum.generate route points to a page which takes two arguments between { }; it corresponds to a Controller (more on that later on), unlike loremipsum.form which points to a (settings) form with a title.

Both routes require permissions, but you can replace them with _access: 'TRUE' for unrestricted access.

loremipsum.links.menu.yml

loremipsum.form:
  title: 'Configure Lorem ipsum'
  description: 'Settings for filler text generation.'
  route_name: loremipsum.form
  parent: system.admin_config_development

While the routing file points to a page at /admin/config/development/loremipsum, the parent definition is needed to list the page under the Administration menu.

README.md

Lorem ipsum
===========

Lorem ipsum generator for Drupal.

Instructions
------------

Lorem ipsum dolor sit amet... **Just kidding!**

This module can be installed without Composer.

Unpack in the *modules* folder (currently in the root of your Drupal
installation) and enable in `/admin/modules`.

Then, visit `/admin/config/development/loremipsum` and enter your own set of
phrases to build random-generated text (or go with the default Lorem ipsum).

Last, visit `www.example.com/loremipsum/generate/P/S` where:
- *P* is the number of *paragraphs*
- *S* is the maximum number of *sentences*

There is also a generator block in which you can choose how many paragraphs and
phrases and it'll do the rest.

If you need, there's also a specific *generate lorem ipsum* permission.

Attention
---------

Most bugs have been squashed, holes covered, features added. But this module
is a work in progress. Please report bugs and suggestions, ok?

Now let's dive into the folders for an in-depth look at specific details.

/config/install/loremipsum.settings.yml

loremipsum:
  page_title: 'Lorem ipsum'
  source_text: "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. \nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. "

This file stores configuration defaults, which are assigned to the correct fields via the next file.

/config/schema/loremipsum.schema.yml

loremipsum.settings:
  type: config_object
  label: 'Lorem Ipsum settings'
  mapping:
    loremipsum:
      type: mapping
      mapping:
        page_title:
          type: text
          label: 'Lorem ipsum generator page title:'
        source_text:
          type: text
          label: 'Source text for lorem ipsum generation:'
block.settings.loremipsum_block:
  type: block_settings
  label: 'Lorem ipsum block'
  mapping:
    loremipsum_block_settings:
      type: text
      label: 'Lorem ipsum block settings'

The schema file is used even if you don't define a custom table for your module; here you can see defaults being assigned to the fields of a configuration form.

While developing this code, I found that populating fields out-of-the-box was one of the most difficult tasks. But fortunately, there's a module for that: Configuration inspector which will help you debug your defaults.

Also, the Schema YML file is super useful in many different ways.

/src/Controller/LoremIpsumController.php

<?php

/**
 * @file
 * Contains \Drupal\loremipsum\Controller\LoremIpsumController
 */

namespace Drupal\loremipsum\Controller;

use Drupal\Component\Utility\Html;
use Drupal\loremipsum\Service\LoremIpsumService;

/**
 * Controller routines for Lorem ipsum pages.
 */
class LoremIpsumController {

  /**
   * Constructs Lorem ipsum text with arguments.
   * This callback is mapped to the path
   * 'loremipsum/generate/{lorem}/{ipsum}'.
   *
   * @var \Drupal\loremipsum\Service\LoremIpsumService $LoremIpsumService
   *   A call to the Lorem ipsum service.
   * @param string $paragraphs
   *   How many paragraphs of Lorem ipsum text.
   * @param string $phrases
   *   Average number of phrases per paragraph.
   */

  // The themeable element.
  protected $element = [];

  // The generate method which stores lorem ipsum text in a themeable element.
  public function generate($paragraphs, $phrases) {
    $LoremIpsumService = \Drupal::service('loremipsum.loremipsum_service');
    $element = $LoremIpsumService->generate($paragraphs, $phrases);

    return $element;
  }

}

Now we're arriving at the core of this module, through a Controller that accesses the Lorem ipsum Service. The dependency injection approach is standard practice for Drupal since version 8, with the added benefits of promoting code reusability, making PHPUnit tests perform better, and producing code that's easier to maintain, since its functionality is decoupled from all of the dependencies. More info on Services and dependency injection in Drupal 8+ here and in the API. As you can see, the method generate inside the LoremIpsumController class relates to the entry in the routing YML file:

_controller: '\Drupal\loremipsum\Controller\LoremIpsumController::generate'

And you'll also need a YML file where your service is defined.

loremipsum.services.yml

services:
  loremipsum.loremipsum_service:
    class: \Drupal\loremipsum\Service\LoremIpsumService
    arguments: []

src/Service/LoremIpsumService.php

<?php

/**
 * @file
 * Contains Drupal\loremipsum\Service\LoremIpsumService
 */

namespace Drupal\loremipsum\Service;

use Drupal\Component\Utility\Html;

/**
 * Service layer for Lorem ipsum generation.
 */
class LoremIpsumService {

  /**
   * Constructs Lorem ipsum text with arguments.
   *
   * @param string $paragraphs
   *   How many paragraphs of Lorem ipsum text.
   * @param string $phrases
   *   Average number of phrases per paragraph.
   */
  public function generate($paragraphs, $phrases) {
    // Default settings
    $config = \Drupal::config('loremipsum.settings');
    // Page title and source text.
    $page_title = $config->get('loremipsum.page_title');
    $source_text = $config->get('loremipsum.source_text');

    // Repertory strategy.
    $repertory = explode(PHP_EOL, str_replace(["\r\n", "\n\r", "\r", "\n"], PHP_EOL, $source_text));

    $element['#source_text'] = [];

    // Generate X paragraphs with up to Y phrases each.
    for ($i = 1; $i <= $paragraphs; $i++) {
      $this_paragraph = '';
      // When we say "up to Y phrases each", we can't mean "from 1 to Y".
      // So we go from halfway up.
      $random_phrases = mt_rand(round($phrases/2), $phrases);
      // Also don't repeat the last phrase.
      $last_number = 0;
      $next_number = 0;
      for ($j = 1; $j <= $random_phrases; $j++) {
        do {
          $next_number = floor(mt_rand(0, count($repertory)-1));
        } while ($next_number === $last_number && count($repertory) > 1);
        $this_paragraph .= $repertory[$next_number] . ' ';
        $last_number = $next_number;
      }
      $element['#source_text'][] = Html::escape($this_paragraph);
    }
    $element['#title'] = Html::escape($page_title);

    // Theme function.
    $element['#theme'] = 'loremipsum';

    return $element;
  }

}

Pictured above is the Service file with the business logic.

The next piece of code gets module settings and stores them for later use:

    // Default settings.
    $config = \Drupal::config('loremipsum.settings');
    // Page title and source text.
    $page_title = $config->get('loremipsum.page_title');
    $source_text = $config->get('loremipsum.source_text');

The above parameters (loremipsum.page_title and loremipsum.source_text) come from the very same variables defined in the settings YAML file (loremipsum.settings.yml), as shown before.

Then we break down the phrases from $source_text into a single array:

    $repertory = explode(PHP_EOL, $source_text);

And use this array to build paragraphs of text:

    $element['#source_text'] = [];

    // Generate X paragraphs with up to Y phrases each.
    for ($i = 1; $i <= $paragraphs; $i++) {
      $this_paragraph = '';
      // When we say "up to Y phrases each", we can't mean "from 1 to Y".
      // So we go from halfway up.
      $random_phrases = mt_rand(round($phrases/2), $phrases);
      // Also don't repeat the last phrase.
      $last_number = 0;
      $next_number = 0;
      for ($j = 1; $j <= $random_phrases; $j++) {
        do {
          $next_number = floor(mt_rand(0, count($repertory) - 1));
        } while ($next_number === $last_number && count($repertory) > 1);
        $this_paragraph .= $repertory[$next_number] . ' ';
        $last_number = $next_number;
      }
      $element['#source_text'][] = Html::escape($this_paragraph);
    }

Note that ['#source_text'] is a render array passed to the template, and that each item in this array goes through Html::escape() for safety.

Finally we give our render array a title, assign a theme function, and return it:

    $element['#title'] = Html::escape($page_title);

    // Theme function
    $element['#theme'] = 'loremipsum';

    return $element;

But before we pass this variable to our template, there are theming functions to be taken care of. You can also add custom CSS and JS in a file called loremipsum.libraries.yml, used to record such dependencies. More details in the appropriate section.

Help improve this page

Page status: No known problems

You can: