2022
Drupal In-Depth

Drupal In-Depth

Translated from German using ChatGPT.

Date: February 2022
Reading time: 14 minutes


After giving an overview of Drupal in my last blog post, I’ll now take a closer look at specific topics.
For this reason, this post is also a bit more advanced.

Custom Block

A block is part of a layout and contains content. Blocks can be placed in any region and then moved afterward. They can include text, a form, or even images.
They are reusable and can be configured to be visible only on specific pages for selected users.

Blocks should not be confused with content types. A content type is a kind of content. It is displayed in the main content area.

Example

A block can be created in two different ways.

Browser

In Drupal, you can add a block under Structure > Block layout > Add custom block.

browser

This can then be placed in a region.

block

It will be displayed like this.

main

Code

Many things can already be configured in the browser. However, you have a bit more flexibility in the code.

hello_block/src/Plugin/Block/HelloBlock.php
<?php
namespace Drupal\hello_block\Plugin\Block;
 
use Drupal\Core\Block\BlockBase;
 
/**
*
* Provides a 'Hello' Block.
* @Block(
* id = "hello_block",
* admin_label = @Translation("Hello block"),
* category = @Translation("Hello World"),
* )
*/
class HelloBlock extends BlockBase {
	/**
	* {@inheritdoc}
	*/
	public function build() {
		return [
			'#markup' => $this->t('Hello, World!'),
		];
	}
}

In line ten, a Block Annotation is used to indicate that this code defines a block.
The following three lines define the id, admin_label, and category. These will then be visible in Drupal.

place-block

The build method is used to create the markup of the block.
This block will be displayed in the frontend as follows.

hello

Regions

Earlier, the block was placed in a region.
Regions are areas that can be styled and positioned. Blocks can then be placed into these areas.

Bartik

I’ll use the Bartik Theme (opens in a new tab) for this example.
All regions are defined in the info file.

web/core/themes/bartik/bartik.info.yml
regions:
	header: Header
	primary_menu: 'Primary menu'
	secondary_menu: 'Secondary menu'
	page_top: 'Page top'
	page_bottom: 'Page bottom'
	highlighted: Highlighted
	featured_top: 'Featured top'
	breadcrumb: Breadcrumb
	content: Content
	sidebar_first: 'Sidebar first'
	sidebar_second: 'Sidebar second'
	featured_bottom_first: 'Featured bottom first'
	featured_bottom_second: 'Featured bottom second'
	featured_bottom_third: 'Featured bottom third'
	footer_first: 'Footer first'
	footer_second: 'Footer second'
	footer_third: 'Footer third'
	footer_fourth: 'Footer fourth'
	footer_fifth: 'Footer fifth'

Once the theme is activated, you’ll see the various areas in Drupal.

regions

You can now style the regions however you like by targeting the correct class using CSS.

web/core/themes/bartik/css/components/breadcrumb.css
/**
* @file
* Styles for Bartik's breadcrumbs.
*/
.breadcrumb {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-size: 0.929em;
}

Hooks

https://drupalize.me/tutorial/what-are-hooks?p=2766 (opens in a new tab)

Hooks are used to change or extend behavior without modifying existing code. With hooks, components can communicate with each other.

You can think of it like this:
A user deletes their account.
Drupal then asks: "The user with ID 19 is deleting their account. Can anyone do something with this information?"
Other modules can now act on this. For example, one module could delete the profile picture, as it's no longer needed. Another could send a feedback survey via email.

A hook is a point where other components can hook into.

There are three types of hooks.

Answer questions

So-called info hooks are responsible for collecting information. These hooks mainly return arrays.
Example: The user_toolbar hook returns the link to a user's personal account, which is then shown in the toolbar.

Alter existing data

This hook modifies already existing data.
An info hook collects a list of information. Then the alter hook is called. This can modify the list before it’s used.
This is probably the most commonly implemented type of hook.

Example: The taxonomy_views_data_alter hook adds taxonomy terms to the article, which show information about the node.

React to events

This type of hook is triggered when an action occurs in the system.

Example: To implement the user deletion case described earlier, you would use the hook_user_cancel hook. It would clean up anything left behind by the user.

Naming

A hook is named like this: HOOK_entity_view()

Here, HOOK is replaced with the module's machine name.
Example: mymodule_entity_view
This hook would be called for every entity type.

But hooks can and should be made more specific. That saves if-conditions in your code.
mymodule_node_view will only be called for nodes.
mymodule_form_2_alter only alters the form with the given ID.

You can find help creating and naming hooks on the Drupal API page (opens in a new tab).

Example

I want some scrolling text to appear in every article.
To do this, I search for the appropriate hook on the Drupal site:
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_view/9.3.x (opens in a new tab)

Next, I create a module. In the .module file, I paste in the found code and adjust it to my needs.
In this case, I add a marquee tag to each node.

web/modules/custom/sneaker/sneaker.module
<?php
/**
* Implements hook_ENTITY_TYPE_view().
*/
function sneaker_node_view(array &$build, \Drupal\Core\Entity\EntityInterface $entity,
\Drupal\Core\Entity\Display\EntityViewDisplayInterface $display, $view_mode) {
	$build['awesome'] = [
		'#markup' => '<marquee>This is awesome.</marquee>',
		'#allowed_tags' => ['marquee'],
	];
}

This is what the articles look like afterward.

article

Preprocess Functions

Preprocess functions allow you to modify or create variables before they are used in template files.

Naming

THEME_preprocess_HOOK()

THEME: Name of the theme or module
HOOK: Template in which the variables are used

For example, if you want to customize the page of the Bartik theme, the function name would look like this:
bartik_preprocess_page()

For an article, the function would be:
mymodule_preprocess_node()

The easiest way to find these names is to view the page in TWIG Debug Mode.

Define title variable

A preprocess function receives a parameter. Through this array, you can then modify the variables used in the template file.

web/themes/custom/mytheme/mytheme.theme
<?php
function mytheme_preprocess_page(&$variables) {
	$variables['title'] = 'My Custom Title';
}
web/themes/custom/mytheme/templates/page.html.twig
{{ title }}

Check author

You can also check other variables. In this case, I check whether the logged-in user created the displayed article. If so, I add "- [you are the author]" to the heading.

web/themes/custom/mytheme/mytheme.theme
<?php
function mytheme_preprocess_node(&$variables) {
	$variables['current_user_is_owner'] = FALSE;
 
	if ($variables['logged_in'] == TRUE && $variables['node']->getOwnerId() == $variables['user']->id()) {
		$variables['label']['#suffix'] = '- [you are the author]';
		$variables['current_user_is_owner'] = TRUE;
	}
}

In the node template file, I display this label on line six.

web/themes/custom/mytheme/templates/node.html.twig
<article{{ attributes.addClass(classes) }}>
	<header>
		{{ title_prefix }}
		{% if label and not page %}
			<h2{{ title_attributes.addClass('node__title') }}>
				<a href="{{ url }}" rel="bookmark">{{ label }}</a>
			</h2>
		{% endif %}
 
		{{ title_suffix }}
 
		{% if display_submitted %}
			<div class="node__meta">
				{{ author_picture }}
				<span{{ author_attributes }}>
				{% trans %}Submitted by {{ author_name }} on {{ date }}{% endtrans %}
				</span>
				{{ metadata }}
			</div>
		{% endif %}
	</header>
 
	<div{{ content_attributes.addClass('node__content', 'clearfix') }}>
		{{ content }}
	</div>
</article>

Webform

You can find the Webform module here: https://www.drupal.org/project/webform (opens in a new tab)

I also recommend enabling the Webform UI module. This UI makes building and managing forms much easier.

webform

Under Structure > Webforms, you can put together a great form with just a few clicks. The configuration options are almost limitless.

structure-webform

For example, you can create pages within seconds to split the form into multiple steps.

Services

What is a service?

A service is a helpful object that does something for you. It can come from your own class or a third-party class. Such an object can be reused anywhere.
Classes that log data or send emails are good examples of services.
A class that represents products (like sneakers or caps) would not be a service. These objects just hold data.

Creating a service

Initial state

You have a controller that prints "Hello World" to the screen.
Unfortunately, such controllers can quickly become messy. Also, the code can only be used in the controller.

modules/shout_hello/src/Controller/HelloController.php
<?php
namespace Drupal\shout_hello\Controller;
use Symfony\Component\HttpFoundation\Response;
 
class HelloController {
	public function hello($count){
		$hello = 'H'.str_ repeat('e', $count).'llo!';
		return new Response($hello);
	}
}

Steps

  1. First, create a folder in src. (e.g., Greeting)

  2. Then, create the class in that folder. (e.g., HelloGenerator.php)

  3. You must define the correct namespace in the class.
    In this case: namespace Drupal\shout_hello\Greeting;

  4. Now move the function into this new class.

    modules/shout_hello/src/Greeting/HelloGenerator.php
    	<?php
    	namespace Drupal\shout_hello\Greeting;
    	class HelloGenerator {
    		public function getHello($length) {
    			return = 'H'.str_ repeat('e', $length).'llo!';
    		}
    	}
  5. Controller

    modules/shout_hello/src/Controller/HelloController.php
    	<?php
    	namespace Drupal\shout_hello\Controller:
     
    	use Drupal\shout_hello\Greeting\HelloGenerator;
    	use Symfony\Component\HttpFoundation\Response;
     
    	class HelloController {
    		public function hello($count){
    			$helloGenerator = new HelloGenerator();
    			$hello = $helloGenerator->getRoar($count);
    			return new Response($hello);
    		}
    	}

Now the logic lives in a separate class. It handles tasks for you. It’s a service. With this version, you still create the class using new HelloGenerator(). This isn’t ideal yet. To improve it, we use the service container.

Service Container

In Drupal, there is an object called Container. This is also referred to as the "Dependency Injection Container." It contains all the services. These include the following classes:

  • Logger factory
  • Translator
  • DB connection
  • File system

To get an overview of all services, you can output them using drupal container:debug in the console.

Initial Situation

Earlier, the object was created directly. However, the container can take care of this for us.

Procedure

Configuring the Service

First, create a file in the root of the module. This file is named: [modulename].services.yml

In this file, configure the service.

modules/shout_hello/shout_hello.services.yml
services:
	shout_hello.hello_generator:
		class: Drupal\shout_hello\HelloGenerator

In the second line, you specify the "Machine Name." This can be made up but must be lowercase and unique. The following line shows where the service is located. Optionally, you can also specify arguments that the service needs.

After clearing the cache, the service will be found in the container.

drupal container:debug | grep shout
Retrieving the Service from the Container
  1. Now, you can inherit from ControllerBase and implement the create method.

    modules/shout_hello/src/Controller/HelloController.php
    <?php
    namespace Drupal\shout_hello\Controller:
     
    use Drupal\Core\Controller\ControllerBase;
    use Drupal\shout_hello\Greeting\HelloGenerator;
    use Symfony\Component\HttpFoundation\Response;
     
    class HelloController extends ControllerBase {
    	public function hello($count){
    		$helloGenerator = new HelloGenerator();
    		$hello = $helloGenerator->getRoar($count);
    		return new Response($hello);
    	}
     
    	public static function create(ContainerInterface $container) {
    	}
    }
  2. Instantiate the object.

    modules/shout_hello/src/Controller/HelloController.php
    <?php
    namespace Drupal\shout_hello\Controller:
     
    use Drupal\Core\Controller\ControllerBase;
    use Drupal\shout_hello\Greeting\HelloGenerator;
    use Symfony\Component\HttpFoundation\Response;
     
    class HelloController extends ControllerBase {
    	public function hello($count){
    		$helloGenerator = new HelloGenerator();
    		$hello = $helloGenerator->getRoar($count);
    		return new Response($hello);
    	}
     
    	public static function create(ContainerInterface $container) {
    		$helloGenerator = $container->get('shout_hello.hello_generator');
    		return new static($helloGenerator);
    	}
    }
  3. Create the constructor and property.

    modules/shout_hello/src/Controller/HelloController.php
    	<?php
    	namespace Drupal\shout_hello\Controller:
     
    	use Drupal\Core\Controller\ControllerBase;
    	use Drupal\shout_hello\Greeting\HelloGenerator;
    	use Symfony\Component\HttpFoundation\Response;
     
    	class HelloController extends ControllerBase {
    		private $helloGenerator;
     
    		public function __construct(HelloGenerator $helloGenerator) {
    			$this->helloGenerator = $helloGenerator;
    		}
     
    		public function hello($count){
    			$helloGenerator = new HelloGenerator();
    			$hello = $helloGenerator->getRoar($count);
    			return new Response($hello);
    		}
     
    		public static function create(ContainerInterface $container) {
    			$helloGenerator = $container->get('shout_hello.hello_generator');
    			return new static($helloGenerator);
    		}
    	}
  4. In the hello method, call the getHello function via the service.

    modules/shout_hello/src/Controller/HelloController.php
    <?php
    namespace Drupal\shout_hello\Controller:
     
    use Drupal\Core\Controller\ControllerBase;
    use Drupal\shout_hello\Greeting\HelloGenerator;
    use Symfony\Component\HttpFoundation\Response;
     
    class HelloController extends ControllerBase {
    	private $helloGenerator;
     
    	public function __construct(HelloGenerator $helloGenerator) {
    		$this->helloGenerator = $helloGenerator;
    	}
     
    	public function hello($count){
    		$hello = $this->helloGenerator->getHello($count);
    		return new Response($hello);
    	}
     
    	public static function create(ContainerInterface $container) {
    		$helloGenerator = $container->get('shout_hello.hello_generator');
    		return new static($helloGenerator);
    	}
    }
Reasons

There are three main reasons why one should put a service in the container:

  • By using a container, the object is only created when it is needed. This allows you to have an abundance of services without facing performance issues.
  • If you need a service multiple times, the same object will be used everywhere. Without the container, a new object would be created each time, which could negatively affect performance.
  • Thanks to the container, you can now pass everything through the constructor. This allows you to use other services within your own.

API

To make queries, I have installed Postman (opens in a new tab). This is not required but is recommended.

JSON

The JSON:API module is usually already installed. It just needs to be enabled if necessary.

Such an API can very quickly make all node data available. To achieve this, not much knowledge is required. However, if you want to access images, for example, multiple calls are needed since you'll only receive the ID initially. Additionally, you will be overwhelmed with data if making a request for many items.

Module

modul

Settings

In the Drupal settings, there are two different configuration options.

operations

Access

To learn exactly how the URL is structured, you can check here: https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/api-overview (opens in a new tab) Here is an example of what a response might look like.

access

REST

While the JSON:API focuses on Drupal's strengths, the REST module offers many more possibilities because it is highly configurable. Additionally, the format, logic, and HTTP method can be chosen freely.

FeatureJSON:APIRESTRemark
Entities exposed as resources✔️✔️REST: needs configuration per entity type. JSON:API exposes everything by default. Both respect entity access.
Custom data exposed as resources✔️Write custom @RestResource plugins. JSON:API only supports entities.
Getting individual resources✔️✔️
Getting lists of resources✔️kindaREST: needs a view with a "REST export" display.
Paginating list of resources✔️REST: not supported! REST export views return all resources. Additional modules like Pager Serializer needed.
Filtering list of resources✔️kindaREST: requires an exposed filter for each field and every possible operator.
Sorting of resources✔️
Includes/embedding✔️Only in HAL+JSON
No unnecessary wrapping of field values✔️REST/HAL normalization exposes raw Drupal PHP structures, causing poor DX. JSON:API simplifies normalization.
Ability to omit fields consumer does not need✔️
Consistent URLs✔️
Consumer can discover available resource types✔️
Drupal-agnostic response structure✔️REST: HAL normalization is theoretically Drupal-free, but practically isn't.
Client libraries✔️
Extensible specWIP
Zero configuration✔️REST: Each @RestResource plugin must be configured (formats, auth, HTTP methods). JSON:API handles this automatically.

Source (opens in a new tab)

Module

To use the RESTful API, you can enable the following two modules.

rest-modul

The first module is required to expose the interface. REST UI provides an interface where you can configure various settings.

Settings

With the RESTful module, you can set permissions for each resource.

permission

Thanks to the UI module, you can find a page under Configuration > Web services > REST where you can open or close all resources.

resource

In my case, I have exposed the content pages.

content

Access

By exposing the nodes, I can then access them.

postman

Custom

The RESTful module is already very good. However, you are more flexible when using code. This is why Drupal allows you to create custom modules.

Such a module must be created by you in code. The following steps are necessary.

  1. Create a new module in the /modules/custom/ folder.

  2. Create an info file.

    demo_rest_api.info.yml
    name: Demo REST API
    description: Define's a custom REST Resource
    package: Custom
    type: module
    core: 8.x
    core_version_requirement: ^8 || ^9
  3. Create a resource class in the /src/Plugin/rest/resource/ folder.

    /src/Plugin/rest/resource/DemoResource.php
    <?php
    namespace Drupal\demo_rest_api\Plugin\rest\resource;
     
    use Drupal\rest\Plugin\ResourceBase;
    use Drupal\rest\ResourceResponse;
     
    class DemoResource extends ResourceBase {
    }
  4. Now you can specify the ID, label, and paths with the "Rest Resource Annotation."

    /src/Plugin/rest/resource/DemoResource.php
    /**
    *
    * Provides a Demo Resource
    * @RestResource(
    * id = "demo_resource",
    * label = @Translation("Demo Resource"),
    * uri_paths = {
    * "canonical" = "/demo_rest_api/demo_resource"
    * }
    * )
    */
    class DemoResource extends ResourceBase {
    }
  5. Now the resource exists. However, it doesn't do anything yet. To catch a GET request, you must implement the corresponding function.

    /src/Plugin/rest/resource/DemoResource.php
    /**
     *
     * Provides a Demo Resource
     * @RestResource(
     * id = "demo_resource",
     * label = @Translation("Demo Resource"),
     * uri_paths = {
     * "canonical" = "/demo_rest_api/demo_resource"
     * }
     * )
     */
    class DemoResource extends ResourceBase {
       /**
       * Responds to entity GET requests.
       * @return \Drupal\rest\ResourceResponse
       */
    	public function get() {
    		$response = ['message' => 'Hello, this is a rest service'];
    			return new ResourceResponse($response);
    		}
    	}
  6. It is also important that the module must be enabled in Drupal.

  7. You can manually make the links accessible in the settings. links Alternatively, you can also add a YAML file to the module. This will be respected upon installation.

    demo_rest_api/config/install/rest.resource.demo_resource.yml
    id: demo_resource
    plugin_id: demo_resource
    granularity: method
    configuration:
    	GET:
    		supported_formats:
    		- json
    		supported_auth:
    		- cookie
    	POST:
    		supported_formats:
    		- json
    		supported_auth:
    		- cookie

Once you have completed all these steps, you can make a request.

Sneaker Loader

src/SneakerLoader.php
namespace Drupal\demo_rest_api;
 
use Drupal\Core\Entity\EntityTypeManagerInterface;
 
class SneakerLoader {
	/**
	* The entity type manager service.
	*
	* @var \Drupal\Core\Entity\EntityTypeManagerInterface
	*/
	protected $entityTypeManager;
 
	/**
	*
	* Constructs a new ContentUninstallValidator.
	* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
	* The entity type manager service.
	*/
	public function __construct(EntityTypeManagerInterface $entity_type_manager) {
		$this->entityTypeManager = $entity_type_manager;
	}
 
	public function getText() {
		/** @var \Drupal\node\NodeInterface $sneaker */
		$sneaker = $this->entityTypeManager->getStorage("node")->load(29);
		return $sneaker->getTitle();
	}
}

In this service, the sneaker with ID 29 is retrieved in the getText method. Its title is then returned. In the DemoResource.php file, where the GET is defined, the getText method is called. The corresponding request would look like this.

demo

SneakerSaver

In my webform, which was described earlier, I have added a so-called post handler. When the form is submitted, a POST request is sent to the specified URL.

remote

In the code, I then created a SneakerSaver class. The contained method creates a sneaker based on the provided data.

demo_rest_api/src/SneakerSaver.php
<?php
namespace Drupal\demo_rest_api;
 
use Drupal\Core\Entity\EntityTypeManagerInterface;
 
class SneakerSaver {
	/**
	*
	* The entity type manager service.
	* @var \Drupal\Core\Entity\EntityTypeManagerInterface
	*/
	protected $entityTypeManager;
 
	/**
	*
	* Constructs a new ContentUninstallValidator.
	* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
	* The entity type manager service.
	*/
	public function __construct(EntityTypeManagerInterface $entity_type_manager) {
		$this->entityTypeManager = $entity_type_manager;
	}
 
	public function saveSneaker($data) {
		/** @var \Drupal\node\NodeInterface $sneaker */
		$sneaker = $this->entityTypeManager->getStorage('node')->create([
		'type' => 'sneaker',
		'title' => $data['title'],
		'uid' => 1,
		'status' => 1
		]);
		$sneaker->save();
		return true;
	}
}

In DemoResource, this method is called in the POST.

demo_rest_api/src/Plugin/rest/resource/DemoResource.php
<?php
namespace Drupal\demo_rest_api\Plugin\rest\resource;
 
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\demo_rest_api\SneakerLoader;
use Drupal\demo_rest_api\SneakerSaver;
 
/**
*
* Provides a Demo Resource
* @RestResource(
* id = "demo_resource",
* label = @Translation("Demo Resource"),
* uri_paths = {
* "canonical" = "/demo_rest_api/demo_resource",
* "create" = "/demo_rest_api/create_entity"
* }
* )
*/
class DemoResource extends ResourceBase {
	private SneakerLoader $sneakerLoader;
	private SneakerSaver $sneakerSaver;
 
	public function __construct(array $configuration, $plugin_id, $plugin_definition, array
	$serializer_formats, LoggerInterface $logger, SneakerLoader $sneakerLoader, SneakerSaver $sneakerSaver) {
		parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
		$this->sneakerLoader = $sneakerLoader;
		$this->sneakerSaver = $sneakerSaver;
	}
 
	public static function create(ContainerInterface $container, array $configuration, $plugin_id,
	$plugin_definition) {
		return new static(
		$configuration,
		$plugin_id,
		$plugin_definition,
		$container->getParameter('serializer.formats'),
		$container->get('logger.factory')->get('rest'),
		$container->get("demo_rest_api.sneaker_loader"),
		$container->get("demo_rest_api.sneaker_saver")
		);
	}
 
	/**
	* Responds to entity GET requests.
	* @return \Drupal\rest\ResourceResponse
	*/
	public function get() {
		$response = ['message' => $this->sneakerLoader->getText()];
		return new ResourceResponse($response);
	}
 
	/**
	*
	* Responds to POST requests.
	* @param mixed $data
	*
	* @return \Drupal\rest\ModifiedResourceResponse
	* The HTTP response object.
	*
	* @throws \Symfony\Component\HttpKernel\Exception\HttpException
	* Throws exception expected.
	*/
	public function post($data) {
		$response = ['message' => $this->sneakerSaver->saveSneaker($data)];
		return new ResourceResponse($response);
	}
}

It is also important that there is a create path under uri_paths, and this URL matches the POST from the webform.

The entire process is described in more detail here (opens in a new tab).