Porting Drupal 7 module to Drupal 8 on the example of Cookies
Time has already passed since the premiere of Drupal 8. New projects and modules are released all the time, and there is an ever-increasing number of projects compatible with version 8.x at drupal.org. Unfortunately, these versions are often still unstable and filled with various bugs. As of today, the number of additional modules available for Drupal 8 is around 2,250, compared to 12,400 Drupal 7 modules. It is immediately visible that the range of ready-made solutions is far more narrow in the case of the new generation, compared to the older one. However, even though in the case of advanced projects, writing them from scratch might be pointless, in the case of smaller add-ons, we can try to port the entire module or its interesting functionalities to version 8.x. Being a Drupal agency, we have to do such drupal development quite often. In this guide, we are going to port a module used by us to display the cookies disclaimer on our website to be used with Drupal 8.
This is what the file structure of v. 7.x looks like. The module is fairly simple, as it contains a custom CSS file, a JavaScript, two templates, one included .inc and the standard .info and. module files.
In order for the script to be visible by Drupal 8, we are going to need an .info.yml file. Let us start with this one. In D8, it is the only file required for the module to be seen by the system.
1. Info files:
Drupal 7
cookiec.info
name = CookieC
description = This module aims at making the website compliant with the new EU cookie regulation
core = 7.x
Drupal 8
cookiec.info.yml
name: CookieC
type: module
core: 8.x
version: 8.x-1.0
description: This module aims at making the website compliant with the new EU cookie regulation
configure: cookiec.settings
The files are fairly similar – as you can see, the main difference here is the file format. Since Drupal 8 uses YML files, the file needs to have an .info.yml extension. Additionally, we also have to add type: because skins are using similar syntax, in our case this is going to be type: module. Additionally, in this file, we can add ‘configure:’ – a routing hook to the configuration page. However, we are going to discuss routing in the next part. It is worth mentioning that – just like in Python – indentations in the code are crucial. After purging the cache, the module should be already visible on the list.
2. .module file and hooks
Let’s now see what can be found in a D7. module file. Our module uses the following hooks/functions:
- hook_menu – used for defining /cookiec page, which displays our cookie policy,
- hook_init – used to initiate our functionality, including the function loading our custom css and js files,
- hook_permission – permission for administering our module,
- hook_theme – template definitions.
In D8 hook_menu, hook_permission and hook_init were done away with.
The tasks of hook_menu are now covered by “module_name.routing.yml”, and instead of hook_init we are going to use EventSubscriber.
3. Hook_menu migration
Drupal 7
Hook menu included in the .module file
<?php
/**
* Implements hook_menu().
*/
function cookiec_menu() {
$items['cookiec'] = array(
//'title' => '',
'description' => 'Cookie policy page.',
'page callback' => 'cookiec_policy_page',
'access arguments' => array('access content'),
'file' => 'cookiec.page.inc',
);
return $items;
}
Drupal 8
Contents of the cookiec.routing.yml file:
cookiec.render_cookiec:
path: '/cookiec'
defaults:
_controller: '\Drupal\cookiec\Controller\Cookiec::renderPage'
_title: ''
requirements:
_permission: 'access content'
The *.routing.yml file contains the name of the routing – cookiec.render_cookiec.
Path: URL address, allowing us to access a given functionality; as in D7 we can also use dynamic paths, such as for example: path: 'example/{user}', in this case it won’t be needed, since our module displays only a static page.
defaults: _controller: '\Drupal\cookiec\Controller\Cookiec::renderPage’ at /cookiec content will be displayed, returned by Cookiec class using renderPage() method. We will write about this in detail while creating /cookiec. static page.
Requirements: We are going to add access permissions in _permission: access content.
We should create all the classes and methods assigned to our routings at once in order to avoid displaying errors.
We are going to create a simple “Hello World” example. Let’s start by creating a Cookiec.php file in /src/Controller:
namespace Drupal\cookiec\Controller;
use Drupal\Core\Controller\ControllerBase;
class Cookiec extends ControllerBase {
function renderPage(){
return array(
'#title' => '',
'#markup' => 'hello word!',
);
}
}
After purging the cache and going to /cookies we should get a page saying “Hello World”.
4. Migrating hook_permissions
In order to create our own permissions, we are going to create module.permission.yml file, similarly to other .yml files:
administer cookiec:
title: 'Administer cookiec administration message module'
description: 'Perform administration tasks for cookiec'
We also add the following to our routing:
requirements:
_permission: 'administer cookiec'
Using the form class is a useful solution in this case.
cookiec.settings:
path: '/admin/config/content/cookiec-settings'
defaults:
_form: '\Drupal\cookiec\Forms\CookiecSettingsForm'
_title: 'cookiec configuration'
requirements:
_permission: 'administer cookiec'
The name of the cookiec.settings routing is going to be placed in the info.yml file (configure: cookiec.settings), thanks to which the module configuration button will redirect us to this form.
5. Hook_init migration
Hook_init, which in D7 started every time the page loaded was removed in D8. In order to get a similar functionality, we are going to use Events, specifically EventSubscriber.
In order to create EventSubscriber in Drupal 8 we are going to need a service. We create service just like other .yml files:
Add the following to cookiec.service.yml:
services:
cookiec_event_subscriber:
class: Drupal\cookiec\EventSubscriber\CookiecSubscriber
tags:
– {name: event_subscriber}
In order for our service to be an Event Subscriber, we need to create a class implementing the interface – EventSubscriberInterface. In order for it to work correctly, we are going to create three methods:
in src/EventSubscriber/CookiecSubscriber.php
/**
* @file Drupal\coociec\EventSubscriber\PopupMessageSubscriber
*/
namespace Drupal\cookiec\EventSubscriber;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class PopupMessageSubscriber
* @package Drupal\cookiec\EventSubscriber
*/
class CookiecSubscriber implements EventSubscriberInterface {
protected $config;
/**
* CookiecSubscriber constructor.
*/
public function __construct() {
$this->config = \Drupal::configFactory()->get('cookiec.settings');
}
public function showCookiecMessage(FilterResponseEvent $event) {
// Check permissions to display message.
$response = $event->getResponse();
if (!$response instanceof AttachmentsInterface) {
return;
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = array('showCookiecMessage', 20);
return $events;
}
}
Load the module settings in our constructor, which will be later passed to the JavaScript.
In order for our event to work, we have to add the getSubscribedEvents() method, put in the name of the method (showCookiecMessage) which will be called and the weight (20). We can use weight to set up the order of the events.
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = array('showCookiecMessage', 20);
return $events;
}
6. Adding JS and CSS files
Our next step will be transferring over our JS and CSS files – in our case they are going to be left almost without any significant changes. Instead, we are going to focus on loading them while executing our code.
If we want to add external JS or CSS files to our module, we can create a file named module.libraries.yml, in our case this is going to be cookiec.libraries.yml
cookiec_library:
version: 1.x
css:
theme:
css/cookiec.css: {}
js:
js/cookiec.js: {preprocess: false}
We added cookiec.css and cookiec.js files here. If we needed to load the existing libraries, we could add them using dependencies, for example:
dependencies:
– core/jquery
In order to load our libraries now, we can use for example hook_preprocess_HOOK. In this case we have to add the following to our variables:
$variables['#attached']['library'][] = 'cookiec/cookiec_library';
In our examples we add our files right as the event loads. We are going to use the following methods to achieve this:
$response = $event->getResponse();
$attachments = $response->getAttachments();
$attachments['library'][] = 'cookiec/cookiec_library';
$response->setAttachments($attachments);
Our module does the majority of its magic client-side. To work properly, our script requires sending several parameters. Settings such as height, width, the displayed text or position are sent to drupalSettings array in JS.
$variables = array(
'popup_enabled' => $config->get('popup_enabled'),
'popup_agreed_enabled' => $config->get('popup_agreed_enabled'),
'popup_hide_agreed' => $config->get('popup_hide_agreed'),
'popup_height' => $config->get('popup_height'),
'popup_width' => $config->get('popup_width'),
'popup_delay' => $config->get('popup_delay')*1000,
'popup_link' => $config->get($language."_link"),
'popup_position' => $config->get('popup_position'),
'popup_language' => $language,
'popup_html_info' => $html_info,
'popup_html_agreed' =>$html_agreed,
);
The values of the variables are taken from the module settings – configs. We are going to discuss this in a bit.
This is how we send our PHP variables to JS.
$attachments['drupalSettings']['cookiec'] = $variables;
$html_info and $html_agreed store html code, obtained by parsing TWIG templates and the variables:
$variables = array(
'title' => 'title',
'message' => $config->get($language."_popup_info"),
);
$twig = \Drupal::service('twig');
$template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_info.html.twig');
$html_info = $template->render($variables);
$variables = array(
'title' => 'title',
'message' => $config->get($language."_popup_info"),
'more' => 't(more)',
'hide' => 't(hide)',
);
$twig = \Drupal::service('twig');
$template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_agreed.html.twig');
$html_agreed = $template->render($variables);
We are going to discuss TWIG in a second.
The entire file with EventSubscriber looks like that:
<?php
/**
* @file Drupal\coociec\EventSubscriber\PopupMessageSubscriber
*/
namespace Drupal\cookiec\EventSubscriber;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\Element;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class PopupMessageSubscriber
* @package Drupal\popup_message\EventSubscriber
*/
class CookiecSubscriber implements EventSubscriberInterface {
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* PopupMessageSubscriber constructor.
*/
public function __construct() {
$this->config = \Drupal::configFactory()->get('cookiec.settings');
}
/**
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
*/
public function showCookiecMessage(FilterResponseEvent $event) {
// Check permissions to display message.
$response = $event->getResponse();
if (!$response instanceof AttachmentsInterface) {
return;
}
// Check module has enable popup
$config = $this->config;
$language = \Drupal::languageManager()->getCurrentLanguage()->getId();
$variables = array(
'title' => 'title',
'message' => $config->get($language."_popup_info"),
);
$twig = \Drupal::service('twig');
$template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_info.html.twig');
$html_info = $template->render($variables);
$variables = array(
'title' => 'title',
'message' => $config->get($language."_popup_info"),
'more' => 'more',
'hide' => 'hide',
);
$twig = \Drupal::service('twig');
$template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_agreed.html.twig');
$html_agreed = $template->render($variables);
$variables = array(
'popup_enabled' => $config->get('popup_enabled'),
'popup_agreed_enabled' => $config->get('popup_agreed_enabled'),
'popup_hide_agreed' => $config->get('popup_hide_agreed'),
'popup_height' => $config->get('popup_height'),
'popup_width' => $config->get('popup_width'),
'popup_delay' => $config->get('popup_delay')*1000,
'popup_link' => $config->get($language."_link"),
'popup_position' => $config->get('popup_position'),
'popup_language' => $language,
'popup_html_info' => $html_info,
'popup_html_agreed' =>$html_agreed,
);
$attachments = $response->getAttachments();
$attachments['library'][] = 'cookiec/cookiec_library';
$attachments['drupalSettings']['cookiec'] = $variables;
$response->setAttachments($attachments);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = array('showCookiecMessage', 20);
return $events;
}
}
7. Configs
Another big change in Drupal 8 is the use of config files. These are also YML files, the goal of which is to facilitate synchronisation of settings, variables, and other data saved in the database between environments.
You can read more about this subject here: https://www.drupal.org/docs/8/configuration-management/managing-your-sites-configuration
The D8-compatible version of our module uses configs to store the settings of our window and to load the default settings during installation. After placing the config file in config/install folder, it is going to be automatically loaded on module installation, so we are not going to use hook_install anymore.
Our config/install/cookiec.settings.yml file”
popup_enabled: 1
popup_agreed_enabled: 1
popup_hide_agreed: 1
popup_width: 100%
popup_delay: '1'
popup_height: '100'
en_popup_title: 'Cookie policy'
en_popup_info: 'This website uses cookies. By remaining on this website you agree to our <a href="/cookiec">cookie policy</a>'
en_popup_agreed: 'I agree'
en_popup_p_private: " <p>This website does not automatical....</p>"
pl_popup_title: 'Polityka cookie'
pl_popup_info: 'Powiadomienie o plikach cookie. Ta strona korzysta z plików cookie. Pozostając na tej stronie, wyrażasz zgodę na korzystanie z plików cookie. <a href="/cookiec">Dowiedz się więcej'
pl_popup_agreed: 'Zgadzam się'
pl_popup_p_private: " <p>Serwis nie zbiera w sposób automatyczny żadnych informacji, z wyjątkiem informacji zawartych w plikach cookies.</p>\r\n <p>Pliki cookies (tzw. „ciasteczka”) </p>"
en_popup_link: /cookiec
pl_popup_link: /cookiec
If you need to load the data in the website’s code, use configFactory() service with get method and provide the name of the config.
$this->config = \Drupal::configFactory()->get('cookiec.settings');
This code was used in the CookiecSubscriber class constructor, giving us quick and easy access to all settings of the module.
We are going to assign them to $variables array
...
'popup_hide_agreed' => $config->get('popup_hide_agreed'),
'popup_height' => $config->get('popup_height'),
'popup_width' => $config->get('popup_width'),
...
8. TWIG templates
One of the most significant changes after moving from D7 to D8 is the fact that D8 did away with PHP templates in favour of TWIG (http://twig.sensiolabs.org/). This is quite an extensive topic in itself, so we are probably going to write a separate article just about it. Right now, the main takeaway for us is that we cannot use PHP functions anymore, and the logic is limited to simple loops and conditions.
Our Drupal 7 module has two templates:
cookiec-agreed.tpl.php
cookiec-info.tpl.php
<?php
/**
* @file
* This is a template file for a pop-up informing a user that he has already
* agreed to cookies.
*
* When overriding this template, it is important to note that jQuery will use
* the following classes to assign actions to buttons:
*
* hide-popup-button – destroy the pop-up
* find-more-button – link to an information page
*
* Variables available:
* – $message: Contains the text that will be display within the pop-up
*/
?>
<div>
<div class ="popup-content agreed">
<div id="popup-text">
<?php print $message ?>
</div>
<div id="popup-buttons">
<button type="button" class="hide-popup-button"><?php print t("Hide this message"); ?> </button>
<button type="button" class="find-more-button" ><?php print t("More information on cookies"); ?></button>
</div>
</div>
</div>
We are going to start with changing the names by replacing the extension with xxx.html.twig.
PHP tags are not parsed in .twig files, so we will have to adapt all functions and comments to the TWIG format.
Comments:
If we want to preserve old or add new comments, we have to replace the PHP tags with {# .... #}.
<#
/**
* @file
* This is a template file for a pop-up informing a user that he has already
* agreed to cookies.
*
* When overriding this template, it is important to note that jQuery will use
* the following classes to assign actions to buttons:
*
*
* Variables available:
* message Contains the text that will be display within the pop-up
* hide – destroy the pop-up
* more – link to an information page
*/
#>
Printing the variable values
We can print the variable by placing our variable in brackets like this: {{ variable }}. Our module has three variables:
message, hide, more – they contain translated strings. Adding {{ message | raw }} will cause the html to be rendered in its pure form, without for example replacing . < > into < > .
<div>
<div class ="popup-content agreed">
<div id="popup-text">
{{ message | raw}}
</div>
<div id="popup-buttons">
<button type="button" class="hide-popup-button"> {{ hide }} </button>
<button type="button" class="find-more-button" > {{ more }} </button>
</div>
</div>
</div>
Logic in TWIG
Our example is fairly simple; however, TWIG allows us to use simple logic. Logic operations are wrapped between {% %} tags.
We can use tags, variable filters and functions.
Here are some examples:
Example tags:
For each loop:
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
IF condition:
{% if online == false %}
<p>Our website is in maintenance mode. Please, come back later.</p>
{% endif %}
Variable operations:
{% set foo = 'bar' %}
Example filters:
We use filters by adding | in {{}} brackets with our variable.
Trim – removes whitespaces or given strings.
{{ ' I like Twig. '|trim }}
{# outputs 'I like Twig.' #}
{{ ' I like Twig.'|trim('.') }}
{# outputs ' I like Twig' #}
Date – date formatting
{{ "now"|date("m/d/Y") }}
{{ post.published_at|date("m/d/Y", "Europe/Paris") }}
Functions:
random() function
{{ random(['apple', 'orange', 'citrus']) }} {# example output: orange #}
{{ random('ABC') }} {# example output: C #}
{{ random() }} {# example output: 15386094 (works as the native PHP mt_rand function) #}
{{ random(5) }} {# example output: 3 #}
Drupal also has a very useful AddClass method, allowing for adding CSS classes to an html element.
{%
set classes = [
'red',
'green',
]
%}
<div{{ attributes.addClass(classes) }}></div>
These are only a few examples and use cases, for more you can read TWIG documentation available at http://twig.sensiolabs.org/documentation.
Additionally, for working with DRUPAL 8 and TWIG it is recommended to also read:
https://www.drupal.org/node/1903374 – Debugging compiled Twig templates
https://www.drupal.org/docs/8/theming/twig/working-with-twig-templates – Working With Twig Templates
https://www.drupal.org/docs/8/theming/twig/twig-template-naming-conventions – Twig Template naming conventions
Sending data to TWIG
As you can see, TWIG offers great capabilities. However, in order for our variables to be seen in our TWIG, we have to get them there.
In our example, we have to save the content of the parsed TWIG to the variable and send it to the JS array. We can do this like that:
First, we collect the variables to be used in our TWIG:
$variables = array(
'title' => 'title',
'message' => $config->get($language."_popup_info"),
'more' => 'more',
'hide' => 'hide',
);
Parsing the template:
$twig = \Drupal::service('twig');
$template = $twig->loadTemplate(drupal_get_path('module', 'cookiec') . '/templates/cookiec_agreed.html.twig');
$html_agreed = $template->render($variables);
However, you will rarely use this method while working with Drupal 8. Most often you will use other methods, like overwriting the standard templates using Twig Template naming conventions.
Blocks:
1. block--module--delta.html.twig
2. block--module.html.twig
3. block.html.twig
Nodes:
1. node--nodeid--viewmode.html.twig
2. node--nodeid.html.twig
3. node--type--viewmode.html.twig
4. node--type.html.twig
5. node--viewmode.html.twig
6. node.html.twig
etc.
Then put the custom templates to our theme/templates folder.
There is also a more advanced method to do the same thing – adding files directly to the modules using hook_theme,
/**
* Implements hook_theme().
*/
function cookiec_theme() {
return array(
'cookiec_agreed' => array(
'template' => 'cookiec_agreed',
'variables' => array(
'title' => NULL,
'body' => NULL,
'read_more' => NULL,
),
),
'cookiec_info' => array(
'template' => 'cookiec_info',
'variables' => array(
'title' => NULL,
'body' => NULL,
'read_more' => NULL,
),
),
);
}
In order to use such a TWIG, our block or site needs to return an array with the #theme key and the variables defined in hook_theme().
Below you can see the example of a block using a custom TWIG
namespace Drupal\hello_world\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Hello' Block
*
* @Block(
* id = "hello_block",
* admin_label = @Translation("Hello block"),
* )
*/
class HelloBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$variables = array(
'title' => 'title',
'body' => 'body',
'read_more' => 'more',
);
return array(
'#theme' => 'cookiec_info',
'#variables' => $variables,
);
}
}
Summary
All functionalities were ported and are now fully compatible with Drupal 8.x. The module works perfectly and is used by us in several projects.
You can download the project from GitHub:
https://github.com/droptica/cookiec/
Leave a comment below if you have any questions or issues.
Summing up our short article: The changes between D7 and D8 are significant, and this is only a small part of the vast scope of innovations and new capabilities offered by our new CMS. If you want to further expand your knowledge about D8 and other tools useful for designing web applications, give us a like on Facebook, where we share our tutorials, guides, and various interesting stuff from the industry. You can also take part in Drupal Day and the Drupal Camp! New content is also coming soon to our blog!