Change record status: 
Project: 
Introduced in branch: 
8.x
Description: 

In Drupal 7, the active theme could be changed either with a 'theme_callback' entry in hook_menu(), or with hook_custom_theme(). In Drupal 8, both methods have been replaced with theme negotiators.

Drupal 7

/**
 * Implements hook_menu()
 */
function example_menu() {
  $items['path'] = array(
    'page callback' => 'foo_bar',
    'theme callback' => 'example_theme_callback',
  );

  return $items;
}

/**
 * Always use stark on that menu route.
 */
function example_theme_callback() {
  return 'stark';
}

Drupal 8

For active themes by route you first have to define a service in your example.services.yml file:

services:
  theme.negotiator.example:
    class: Drupal\example\Theme\ThemeNegotiator
    tags:
      - { name: theme_negotiator, priority: 0 }

and then in "example/src/Theme/ThemeNegotiator.php" use that new defined service class:

namespace Drupal\example\Theme;

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

class ThemeNegotiator implements ThemeNegotiatorInterface {
  /**
   * {@inheritdoc}
   */
  public function applies(RouteMatchInterface $route_match) {
    // Use this theme on a certain route.
    // return $route_match->getRouteName() == 'example_route_name';

    // Or use this for more than one route:
    $possible_routes = array(
        'entity.taxonomy_term.add_form',
        'entity.taxonomy_term.edit_form'
    );

    return (in_array($route_match->getRouteName(), $possible_routes));
  }

  /**
   * {@inheritdoc}
   */
  public function determineActiveTheme(RouteMatchInterface $route_match) {
    // Here you return the actual theme name.
    return 'stark';
  }

}

For hook_custom_theme() you can use the same mechanism. If you want to take over all existing routes you have to choose a high priority.
In case you want to just change the default theme dynamically but let route specific theme negotiators still apply, use a priority < 0.

Impacts: 
Module developers
Themers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Theming guide done
Module developer documentation: 
Module developer documentation done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done

Comments

stefanos.petrakis’s picture

Shouldn't applies() be written as applies($route_match) in the above example?

yannickoo’s picture

I've fixed that :)

Aleksey Pavlyuk’s picture

In my case I had to add "services:" string to the first line of service file, otherwise I get a parsing error (D8.2.4):

services:
  theme.negotiator.example:
  ...
RAWDESK’s picture

I only succeeded in getting this to work by also providing a __construct() method inside my ThemeNegotiator class.
Otherwhise core (8.3.7) wouldn't recognize both applies() as determineActiveTheme() methods.
Since they are called from within core/lib/Drupal/Core/Theme/ThemeNegotiator.php based on the available (sorted) list of Negotiators, the custom developed needed to be constructed first via createService() method in core/lib/Drupal/Component/DependencyInjection/Container.php

This createService() method relies on the services.yml files, as the one provided in the above example.
Mine looks like :

services:
  theme.negotiator.custom_themeswitcher:
    class: Drupal\custom_themeswitcher\Theme\CustomThemeswitcherNegotiator
    arguments: ['@request_stack']
    tags:
      - { name: theme_negotiator, priority: 1000 } 

with an additional argument @request_stack that will be received in the __construct() method of your Negotiator class.