Installing Drupal 8 with Composer and Docker

By Ronald van Belzen | February 29, 2020

The preferred way to install Drupal 8 is with the aid of the PHP package manager Composer (Installing Drupal 8). When you also want to use Docker to create a local development environment you have several ways to approach this challenge. I will give an example of one such approach to set up an environment for Drupal development with Nginx, MariaDB, MailHog and XDebug.


When you are using a Mac or Windows 10 (Professional or Enterprise Edition) you just need to install Docker Desktop on you machine. For Linux visit the Docker site to determine the tooling you need to install.

When you have Docker Desktop installed and running you need set the disks you want to share in the settings Resources / File Sharing.

Installing Drupal

I did not install Composer on my machine, because I am going to use the Composer docker image from Docker Hub. For that I open my command-line interface and navigate to the directory in which I want to create the subdirectory for my Drupal project and execute the following command:

docker container run --rm --interactive --tty --volume ${PWD}:/app composer create-project --no-install drupal/recommended-project myproject

This command will pull the Composer docker image and create and run the container for this image and execute the composer create-project command without installing Drupal. But it will create the subdirectory "myproject" and the "composer.json" and "composer.lock" files. The "--rm" parameter will stop and remove the container after the command finishes.

First we need to change the "composer.json" in an editor by replacing the config parameter by the following config:

    "config": {
        "sort-packages": true,
        "platform": {
            "php": "7.2.28",
            "ext-gd": "1"

You may need to change the php version in your case (or leave it out) and you may need to include php extensions later on, but the "ext-gd" extension is required to be able to install the minimal version of Drupal 8. The Composer installation will fail without it.

Best also delete the "composer.lock" before installing Drupal with composer.

REST API for decoupled Drupal 8 User registration confirmation

By Ronald van Belzen | December 27, 2018

For decoupled Drupal there already exist REST API's for user login, user logout and user registration (the latter since Drupal 8.3). However, Drupal does not yet deliver REST API's for password reset and user registration confirmation. The password reset API is easy to create, but I will give an example for the API anyway as a starter.

The POST for the password reset expects a JSON in the body that looks like this:

    "mail": { "value": "" }

As you can see the mail address is used for the identification of the user to which an e-mail will be send. When you need the ability to (also) use the user login name, you will need to adapt the example below.

The API does not actually reset the password. It sends the user an e-mail that can be used to visit a form at an url where the user can change her or his password (without being logged in).

REST API for file upload in Drupal 8 (multipart/form-data)

By Ronald van Belzen | November 30, 2018

This article is related to a former article in which I claimed that "there is no support to directly upload images using REST in Drupal 8". Since Drupal 8.6 this in no longer true ( You can read how to use this new functionality at and

This article is, however, about an entirely different method to upload a file in Drupal 8 for the somewhat unusual use case of passing the uploaded file to a third party REST api. Not that the method described here will be limited to that use case, but in most cases the new Drupal 8.6 functionality will be the preferred choice.

The situation was that for a decoupled set-up the front-end needed to upload a file to a third party REST api. For that upload the third party needed info about the user present in Drupal that the front-end was not allowed to retrieve or expose in any way. Also, as a temporary solution Drupal needed to perform a virusscan on the uploaded files. The front-end strongly preferred the use of a multipart/form-data content type for the POST request.

So, the uploaded file is not meant for storing in the Drupal application and not meant to be saved as File object. The method shown below does do that anyway, because the clamav module, used for the virus scan, requires it.

As usual I start with a new module:


name: My REST Service
type: module
description: "REST Service for passing file upload"
package: Web services
core: '8.x'

And since I need to create the end-point for this REST api myself, I create the route for this end-point:

How to define a local task in Drupal 8

By Ronald van Belzen | June 2, 2018

A local task in Drupal is a callback displayed as a tab. A local task must have a parent item in order for the tab to be rendered.

A well-known example that is part of Drupal core are the local taks for comments with the menu titles "Published comments" and "Unapproved comments". To demonstrate I am going to add an extra local task to those of comments. Let's say I made a new view that will display spam comments and in the advanced settings I have given that view the machine name "page_comment_spam".

The first step would be to create the routing to that view. In the routing the machine name of the view is given to the defaults _view parameter.

# mycomment.routing.yml

  path: /admin/content/comment/spam
    _title: 'Comment spam'
    _view: page_comment_spam
    _permission: 'administer comments'

This routing defines the callback I will need to define the local task. Instead of a view, I could also have define a controller function (with a defaults _controller parameter) or a form (with a defaults _form parameter), which is used more often. For this example it does not really make a difference.

The definition for the local task is as follows.

# mycomment.links.tasks.yml

  title: 'Spam comments'
  route_name: mycomment.admin_comment_spam
  class: Drupal\mycomment\Plugin\Menu\LocalTask\SpamComments
  parent_id: comment.admin
  weight: 10

I hope that the fact that I named the routing name and the task name the same, does not confuse you in thinking that it needs to be the same name. It does not need to be.

Special in the above is that a class is being defined. This class definition is optional. I included it here to demonstrate how you can dynamically change the title of the tab with the help of a local task plugin.

What is important is that I defined the parent_id the same as the existing local tasks for comments, and that I defined the route_name to be used for the callback of the tab. I added some weight to the definition to make sure the tab is displayed to the right of the existing local tasks.

The plugin definition I adapted from the one being used in the (core) comments module. It adds a count to the tab link equal to the number of comments that will be displayed in the view.

Reporting spammers to SFS

By Ronald van Belzen | May 17, 2018

The next step is reporting spam to This will be an action initiated by a maintainer who has spotted spam, preferably by one click on a button. This action will need to be handled by the software by sending the report to We we look at the latter first and adding buttons to start the report after that.

As an example we concentrate on comments. The function that needs to be called (commentReport()) first checks whether there is a token (or api key) defined. It also checks whether the user is anonymous. You cannot report an anonymous comment, since requires you to fill in name, e-mail address and ip address to report spam and for an anonymous comment post you only have the ip address.

Blocking spammers with SFS

By Ronald van Belzen | May 13, 2018

Let's focus our attention on nodes. Core functionality does not save the IP address of the user together with the node when it is created, so we need to introduce that to our module. The most straightforward approach would be to save the IP address in our own database table. For that I need to introduce a database table (sfs_hostname) by defining a new entity called SfsHostname.

/* /src/Entity/SfsHostname.php */

namespace Drupal\sfs\Entity;

use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;

 * Defines the sfs hostname entity.
 * @ContentEntityType(
 *   id = "sfs_hostname",
 *   label = @Translation("SFS Hostname"),
 *   base_table = "sfs_hostname",
 *   entity_keys = {
 *     "id" = "id",
 *     "uuid" = "uuid",
 *     "label" = "hostname",
 *   },
 *   handlers = {
 *     "storage_schema" = "Drupal\sfs\SfsHostnameStorageSchema",
 *   },
 *   admin_permission = "administer sfs",
 * )
class SfsHostname extends ContentEntityBase implements ContentEntityInterface {

   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   * @return array|\Drupal\Core\Field\FieldDefinitionInterface[]|mixed
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {

    $fields['id'] = BaseFieldDefinition::create('integer')

    $fields['uuid'] = BaseFieldDefinition::create('uuid')

    $fields['hostname'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Host name'));
    $fields['uid'] = BaseFieldDefinition::create('integer')
    ->setLabel(t('User ID')); //index

    $fields['entity_id'] = BaseFieldDefinition::create('integer')
    ->setLabel(t('Entity ID'));

    $fields['entity_type'] = BaseFieldDefinition::create('string')
    ->setLabel(t('Entity type'));

    $fields['created'] = BaseFieldDefinition::create('created')
    ->setLabel(t('Creation date'));

    return $fields;

Just in case in the future we might be interested in saving IP addresses for other entity types than nodes, I included the field "entity_type" to make that possible. I also included indexes to speed up the lookup of IP addresses. These are defined in the storage handler SfsHostnameStorageSchema to wich is reffered in the annotation of SfsHostname.

Writing the client for the Stop Forum Spam API

By Ronald van Belzen | May 10, 2018

Reading the description of the Stop Forum Spam api usage made me decide to use what is called "Multiple queries", which means checking the existence of a username, e-mail address and the IP address of a potential spammer in their database in a single call.

The number of response formats is large enough when it contains json. So I will use json and since I will use https over http when there is a choice, I picked https.

In the response I will concentrate on the "success", "appears" and "frequency" values and ignore the "confidence" score for now. It seems to me that just as many low as high confidence spammer are knocking at my door lately. So, it needs some more investigating before I can use that value in discriminating spammers from IP addresses formerly owned by spammers

The Client itself will be implemented as a service, allowing me to let Drupal do the heavy lifting with dependency injections. I will also add the modules very own cache bin with the name "sfs", because I plan to cache the api calls to For this purpose I added the configuration parameter "sfs_cache_duration" to the module to allow administrators to set the cache time to their needs.

    class: Drupal\sfs\SfsRequest
    arguments: ['@config.factory', '@current_user', '@logger.factory', '@http_client', '@database', '@cache.sfs']
    class: Drupal\Core\Cache\CacheBackendInterface
      - { name: cache.bin }
    factory: cache_factory:get
    arguments: [sfs]

The heart of the service will be the isSpammer() method that will the determination whether a user is a spammer or not.

Making a contributing module for fighting spam

By Ronald van Belzen | May 9, 2018

Making a new module starts with an idea for the module. In this case it was trying to make a module that can replace Mollow to some extend (see previous blog post).

Finding a name for your module can be a challenge, but whatever name you pick, be sure that the machine name of your module is available. Try whether the project exist by visiting{my_module_name}. A page not found (404) response is a good enough indicator to confirm that your module name is still available.

Next step is to read the documentation. The best starting point seems to be Contribute to development. To gather all the information you need to follow half a dozen links, but as far as I can tell all the information is there.

I had a look at the Mods & Plugins that make use of the service provided by Stop Forum Spam, and the concensus seems to be to name the mod after the service it makes use of. So I went for the name Stop Forum Spam Client. Don't go there before I finish this series of articles. I will release a fully functional and tested version soon after that.

name: 'Stop Forum Spam Client'
type: module
description: 'Client that makes use of the api services for blocking spam, spammers and spambots.'
configure: sfs.settings_form
package: 'Spam control'
version: '1.0'
core: '8.x'

As you can see in the above info file the module has no dependencies, while the module may depend on the presence of some modules, but must be able to be installed in their absence. The configuration setting form will imediately show a way to solve this dilemma. And what these modules are will become clear when looking at the configuration install parameters.

Fighting Spam

By Ronald van Belzen | May 7, 2018

I was a satisfied user of the Mollom module, but unfortunately nothing lasts forever, and about a year ago it was announced that the service would be discontinued. Not long ago the moment arrived and my site was hit by spam. It was time to look for an alternative for Mollom.

Jeff Geerling wrote quite a good article in which he found his replacement for Mollom: CleanTalk. It is not free, but relative cheap. According to Jeff it has some restrictions but it fulfills his requirements and works like a charm.

Personally, I have been a years long follower of the StopForumSpam site and did remember the existence of the SpamBot module for Drupal 7, that has been ported to Drupal 8 (and is still in beta, but it works). The only restriction of the SpamBot module is that it is designed to stop spammers from registering  and nothing more. The drupal 7 version can also report spammers (the port to Drupal 8 is missing the latter functionality), but it certainly is not a full replacement for Mollom. However, something is better than nothing, so I gave it a try.

Together with the Honeypot module and the Captcha module it is quite effective, so I cannot complain. But, as is so often the case, I want more. Like: being able to block spammers/spambots from posting content and comments (and the ability to report the spammers that get through to StopForumSpam). Maybe I can develop that myself. If I would manage to include blocking spam to Webform I think that would be a decent alternative for Mollom. Maybe it is not even that hard to do.

So, I decided to find out, and keep you posted of my progress.

Turn Drupal 8 into an Identity Provider (continued)

By Ronald van Belzen | December 13, 2017

In a previous blog post I described how to turn a Drupal 8 installation into a Identity Provider (IdP) by configuring SimpleSAMLphp. The configuration files where placed in a subdirectory of the vendor map, which is something you really should not do when you are using Composer to install and update your Drupal installation.

Move the configuration files

When you move these configuration files to another location SimpleSAMLphp should be told about it. For this purpose the environment variable SIMPLESAMLPHP_CONFIG_DIR exists. To my experience the best way to set this variable is in the Apache vhost.conf file. In my case I moved the configuration files to /var/www/drupalvm/drupal/web/sites/default/simplesamlphp:

<VirtualHost *:80>
  DocumentRoot "/var/www/drupalvm/drupal/web"
  Alias /simplesaml "/var/www/drupalvm/drupal/vendor/simplesamlphp/simplesamlphp/www"
  SetEnv SIMPLESAMLPHP_CONFIG_DIR "/var/www/drupalvm/drupal/web/sites/default/simplesamlphp"
  <Directory "/var/www/drupalvm/drupal/web">
    AllowOverride All
    Options -Indexes +FollowSymLinks
    Require all granted
  <FilesMatch \.php$>
    SetHandler "proxy:fcgi://"

The files 'config.php' and 'authsources.php' are placed in this subdirectory (not is a subdirectory ./config of this subdirectory).

In this subdirectory the subdirectories ./cert, ./metadata and ./modules are created. The ./cert subdirectory contains the certificates we created in the previous blog post. The ./metadata subdirectory contains the files 'saml20-idp-hosted.php' and 'saml20-sp-remote.php' that were also created in the previous blog post. The ./modules directory just contains the ./modules/drupalauth subdirectory with the empty file 'default-enable'.

Next we need to tell SimpleSAMLphp where to find the certificates and the metadata in the 'config.php':