How to change theme by Role in Drupal 8.x

24 / Nov / 2016 by Harsh Behl 0 comments

How to change theme by Role in Drupal 8.x

Even before I start, I would like to shed some light on what are Themes in Drupal. A theme is a bunch of files that defines the presentation layer. There are many contributed themes in Drupal project that can be found under ‘Themes’. For a programmer, there are 2 kinds of themes – Contributed & Custom Themes.

  1. A contributed theme is what you download from Drupal.org and install in your Drupal package.
  2. A custom theme is what you build your own either from a base or custom theme.

So the question arises what’s the relation between roles & themes and why there is a need to switch themes for different roles.

Role in Drupal is used to manage user permissions on the website. A set of permissions is assigned to a role and these roles can be assigned to users.  So basically, different roles mean different permissions related to that role.

This is actually a feature request that arises on demand/need. Also if you are familiar with Drupal 7/8 then you must have seen a different theme for admin role (theme name used is seven) & for roles anonymous or authenticated (theme name used is Bartik). Now let’s take an example of having 9-10 different roles on a Drupal site, all are sharing the same theme i.e Bartik. If in such a scenario a need arises to have different themes (layers/presentations) for different roles, a user with role A having theme Bartik (Drupal core theme name) and User with Role B needs to have classy (Drupal core theme name) theme. Then, for achieving the above state, the process explained below in the article can be used.

Earlier in Drupal 7, there were only two files i.e module_name.info and module_name.module that were easy to understand and configure. However, with the launch of Drupal 8  in November 2015, the architecture is changed from procedural to MVC.

Now, with new PHP architecture in Drupal 8 which is based on symphony allows us to learn new things. For Instance, the new twig system in theming restricts writing custom PHP scripts in templates which was earlier possible with Drupal 7.

OOPs concepts, classes etc.have also been introduced in Drupal 8. However to develop a new module or theme in Drupal. There are some key points that a developer should keep in mind.

  1. There are many contributed modules/themes that are present in Drupal. So, there is no need to create a new one. Also, there is a module that is present for only Drupal 7 version but, not for Drupal 8. And also contributing to open source platforms like Drupal enhances your company’s profile on Drupal.org.
  1. What was the process in Drupal 7 ? The approach to changing theme dynamically on runtime needs to change completely as the architecture of Drupal 8 has changed a lot. Also, the hook system will be dropped entirely in Drupal 9. So, keeping in mind this whole thing. There is a need to search a new approach on Drupal.org. ‘Change in Records‘ is where you can explore the changes in previous versions and the new things introduced.
  1. Searching in Change in Records: By searching for new changes in Drupal 8, I found some useful links like hook_custom_theme is replaced by theme callback. Now, that I had a starting point so, I began my search for theme negotiators. Theme Negotiators are responsible for any alterations on themes.

CODE:

  1. Build the module architecture first
  2. Create a module folder inside ROOT/modules/custom/role_based_theme_switcher
  3. Create a role_based_theme_switcher.info.yml file, the contents in the file are as follows:
	name: Role Based Theme Switcher
	description: This module is for switching the theme according to Role
	package: Custom

	type: module
	core: 8.x
	
  1. Create role_based_theme_switcher.services.yml file:
services:
    theme.negotiator.role_based_theme_switcher:
	class: Drupal\role_based_theme_switcher\Theme\RoleNegotiator
	tags:
  	- { name: theme_negotiator, priority: 10 }

If you want to provide the form settings with the configuration you should follow step 3 & 4 otherwise skip.

  1. Create routing file the location will be:

ROOT/modules/role_based_theme_switcher/role_based_theme_switcher.routing.yml

    role_based_theme_switcher.settings:
	path: '/admin/structure/role_based_theme_switcher/settings'
	defaults:
  	_form: '\Drupal\role_based_theme_switcher\Form\AdminSettingsForm'
  	_title: 'URL Setting'
	requirements:
  	_permission: 'administer site configuration'
  1. Define your configuration file, which needs to be placed at the following location:

ROOT/modules/role_based_theme_switcher/config/install/role_based_theme_switcher.RoleBasedThemeSwitchConfig.yml

You can define your own configuration that we will be using for storing the form information submitted by the user.

Themeroll:

Remember you can define any name within the config file like:

role_based_theme_switcher.config:

5. Create a configuration form in admin:

The for file should be placed at a specific location.
ROOT/modules/role_based_theme_switcher/src/Form/AdminSettingsForm.php

Code:

<?php

namespace Drupal\role_based_theme_switcher\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\Entity;
use Drupal\user\Entity\Role;
use Drupal\Core\Extension\ThemeHandler;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Configure Role Based settings for this site.
 *
 * @category Role_Based_Theme_Switcher
 *
 * @package Role_Based_Theme_Switcher
 *
 * @author pen <author details>
 *
 * @link https://www.drupal.org/sandbox/pen/2760771 description
 */
class AdminSettingsForm extends ConfigFormBase {
  protected $themeGlobal;
 /**
   * {@inheritdoc}
   */
  public function __construct(ThemeHandler $themeGlobal) {
  $this->themeGlobal = $themeGlobal;
  }
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
	return new static(
      $container->get('theme_handler')
	);
  }
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
	return 'role_admin_settings';
  }

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
	return [
      'role_based_theme_switcher.RoleBasedThemeSwitchConfig',
	];
  }

  /**
   * {@inheritdoc}
   *
   * Implements admin settings form.
   *
   * @param array $form
   *   From render array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Current state of form.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
	$themes = $this->themeGlobal->listInfo();
	$themeNames = array('' => '--Select--');
	foreach ($themes as $key => $value) {
      $themeNames[$key] = $key;
	}
	$roles = Role::loadMultiple();
	$roleThemes = $this->config('role_based_theme_switcher.RoleBasedThemeSwitchConfig');
    $form['role_theme'] = array(
  	'#type' => 'table',
  	'#header' => array(
    	t('Label'),
    	t('Themes List'),
    	t('Weight'),
  	),
  	'#empty' => t('There are no items yet. Add an item.', array()),
  	// TableDrag: Each array value is a list of callback arguments for
  	// drupal_add_tabledrag(). The #id of the table is automatically prepended;
  	// if there is none, an HTML ID is auto-generated.
  	'#tabledrag' => array(
    	array(
      	'action' => 'order',
          'relationship' => 'sibling',
      	'group' => 'role_theme-order-weight',
    	),
  	),
	);
	if (!empty($roleThemes->get('roletheme'))) {
  	$roleThemes = $roleThemes->get('roletheme');
  	if (count($roleThemes) == count($roles)) {
    	$roles = $roleThemes;
  	}
  	elseif (count($roleThemes) != count($roles)) {
    	foreach ($roles as $rem_key => $rem_val) {
      	if (!array_key_exists($rem_key, $roleThemes)) {
            $merge[$rem_key] = ['id' => '', 'weight' => 10];
            $roleThemes = array_merge($roleThemes, $merge);
      	}
    	}
    	$roles = $roleThemes;
  	}
	}
	else {
  	$roleThemes = [];
  	$i = 0;
  	foreach ($roles as $roles_key => $roles_val) {
    	if ($roles_key == 'administrator') {
          $roleThemes[$roles_key]['weight'] = $i;
          $roleThemes[$roles_key]['id'] = 'seven';
      	$i++;
    	}
    	else {
          $roleThemes[$roles_key]['weight'] = $i;
          $roleThemes[$roles_key]['id'] = 'bartik';
      	$i++;
    	}
  	}
  	$roles = $roleThemes;
	}
	// Build the table rows and columns.
	foreach ($roles as $id => $entity) {
  	// TableDrag: Mark the table row as draggable.
      $form['role_theme'][$id]['#attributes']['class'][] = 'draggable';
  	// TableDrag: Sort the table row according to its existing/configured weight.

  	// Some table columns containing raw markup.
      $form['role_theme'][$id]['label'] = array(
    	'#plain_text' => ucfirst($id) . t(' user'),
  	);

      $form['role_theme'][$id]['id'] = array(
    	'#type' => 'select',
    	'#title' => t('Select Theme'),
    	'#options' => $themeNames,
        '#default_value' => (string) $roleThemes[$id]['id'],
  	);
  	// TableDrag: Weight column element.
      $form['role_theme'][$id]['weight'] = array(
        '#type' => 'weight',
        '#title_display' => 'invisible',
        '#default_value' => (int) $roleThemes[$id]['weight'],
    	// Classify the weight element for #tabledrag.
    	'#attributes' => array('class' => array('role_theme-order-weight')),
  	);
	}
	$form['actions'] = array('#type' => 'actions');
    $form['actions']['submit'] = array(
  	'#type' => 'submit',
  	'#value' => t('Save changes'),
	);

	return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
	$rollTheme = $form_state->getValue('role_theme');
	foreach ($rollTheme as $key => $value) {
  	if (in_array((int) $value['weight'], $role_arr)) {
        $form_state->setErrorByName('role_theme', $this->t("Thers is an error in the form."));
  	}
  	$role_arr[$key] = (int) $value['weight'];
	}
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
	$rollTheme = $form_state->getValue('role_theme');
	foreach ($rollTheme as $key => $value) {
  	$role_arr[(int) $value['weight']] = ['theme' => $value['id'], 'role' => $key];

  	$roles[] = $key;
	}

	ksort($role_arr);

	foreach ($role_arr as $new_key => $new_value) {

  	if (in_array($new_value['role'], $roles)) {
        $roll_array[$new_value['role']] = ['id' => $new_value['theme'], 'weight' => $new_key];
  	}
	}
	$roleThemes = $this->config('role_based_theme_switcher.RoleBasedThemeSwitchConfig');
    $roleThemes->set('roletheme', $roll_array);
    $roleThemes->save();
    drupal_set_message("Role theme configuration saved succefully");
	// clearing cache for anonymous users.
    drupal_flush_all_caches();
  }

}

Remember all the themes loaded above are dynamically loaded by using services “theme_handler” and dependency injection through a variable “protected $themeGlobal;”. This is important to note that loading services using defense injection is a good practice instead of loading them statically for ex:

Static method:

\Drupal::service(‘theme_handler’)->listInfo();

Dynamic method:

 

  protected $themeGlobal;
  /**
   * {@inheritdoc}
   */
  public function __construct(ThemeHandler $themeGlobal) {
  $this->themeGlobal = $themeGlobal;
  }
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
	return new static(
      $container->get('theme_handler')
	);
  }

6. As now Drupal 8 follows PSR4 standard the files needs to be placed in specific location.

So, RoleNegotiator.php file should goto ROOT/modules/custom/role_based_theme_switcher/Theme/RoleNegotiator.php

<?php

namespace Drupal\role_based_theme_switcher\Theme;

use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Theme\ThemeNegotiatorInterface;

/**
 * Sets the active theme on admin pages.
 */
class RoleNegotiator implements ThemeNegotiatorInterface {

  // Protected theme variable to set the default theme if no theme is selected.
  protected $theme = NULL;

  /**
   * Whether this theme negotiator should be used to set the theme.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The current route match object.
   *
   * @return bool
   *   TRUE if this negotiator should be used or FALSE to let other negotiators
   *   decide.
   */
  public function applies(RouteMatchInterface $route_match) {
  // Use this theme on a certain route.
  $change_theme = TRUE;
  $route = \Drupal::routeMatch()->getRouteObject();
  $is_admin_route = \Drupal::service('router.admin_context')->isAdminRoute($route);
  $current_user = \Drupal::currentUser();
  $user_roles = $current_user->getRoles();
  $has_admin_role = FALSE;
  if (in_array("administrator", $user_roles)) {
    $has_admin_role = TRUE;
  }
  if ($is_admin_route === TRUE && $has_admin_role === TRUE) {
    $change_theme = FALSE;
  }
  // Here you return the actual theme name.
  $roleThemes = \Drupal::config('role_based_theme_switcher.RoleBasedThemeSwitchConfig')->get('roletheme');
  // Get current roles a user has.
  $roles = \Drupal::currentUser()->getRoles();
  // Get highest role.
  $theme_role = $this->getPriorityRole($roles);
  $this->theme = $roleThemes[$theme_role]['id'];

  return $change_theme;
  }

  /**
   * Determine the active theme for the request.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The current route match object.
   *
   * @return string|null
   *   The name of the theme, or NULL if other negotiators, like the configured
   *   default one, should be used instead.
   */
  public function determineActiveTheme(RouteMatchInterface $route_match) {
  return $this->theme;
  }

  /**
   * Function to get roles array and return highest priority role.
   *
   * @param array $roles
   *   Array of roles.
   *
   * @return string $theme
   *   Return role.
   */
  public function getPriorityRole($roles) {
  $roleThemes = \Drupal::config('role_based_theme_switcher.RoleBasedThemeSwitchConfig');
  foreach ($roleThemes->get('roletheme') as $key => $value) {
    if (in_array($key, $roles)) {
      $theme[$key] = $value['weight'];
    }
  }
  $theme = array_search(max($theme), $theme);
  return $theme;
  }

}

Last for creating a link in admin configuration you can include a role_based_theme_switcher.links.menu.yml file at ROOT/modules/role_based_theme_switcher/ location:
code:
Last for creating a link in admin configuration you can include a role_based_theme_switcher.links.menu.yml file at ROOT/modules/role_based_theme_switcher/ location:
code:

role_based_theme_switcher.role_theme:
title: Role Theme
route_name: role_based_theme_switcher.settings
description: 'Manage theme for roles.'
parent: system.admin_config_system
weight: 10

Notes: After completing the file structure and code development don’t forget to enable the module under “abc.com/admin/modules” and clear Drupal’s cache “abc.com/admin/config/performance”.

Conclusion:

“abc.com” is an example website path.

In the above code, there is a scenario of how to create a module, a route, a service, a form with all functionality needed to change the Theme. You can change the logic in RoleNegotiator.php according to your needs i.e changing of themes per user or by menu URLs or by any other logic. If the requirements & form fields are different then, code may be modified in AdminSettingsForm.php. Instead of using Database storage we have used Drupal 8 new feature, Configuration. All the details filled in AdminSettingsForm.php will be saved in configuration & same can be retrieved with writing a database query. The code in the file role_based_theme_switcher.links.menu.yml will provide a link under system configuration under abc.com/admin/config with a name Role Theme which will redirect to abc.com/admin/structure/role_based_theme_switcher/settings path you can find the form for selecting different themes for different roles.

FOUND THIS USEFUL? SHARE IT

Leave a comment -