RouteSubscriber: serve a different page on an existing URL

Often when already existing pages/forms have to be changed, you simply use one of the alter hooks to add the needed extra functionality. However, sometimes you want, or are forged, to overwrite an already existing page completely. Applying unset() on each element (or even re-initialize the $form array) is clearly not the solution you want to implement. Luckily, a different solution feels definitely far more correct: Drupal’s RouteSubscriber.

Basically it comes down to register an alternative function that Drupal invokes when constructing its routing table. Using this function you can change/add the different aspects you normally write in your modules .routing.yml file. As an example, let’s assume that you want to show a confirmation form before a log out is performed. The log out procedure is triggered when the user navigates to user/logout, e.g., when clicking on the log out button in the user account menu. This route is defined in Drupal’s core user module under the name user.logout.

The example class below, UserLogoutConfirmRouteSubscriber, implements the alterRoutes() function and replaces the controller with a custom form (i.e., UserLogoutConfirmForm) for the user.logout route.

<?php

namespace Drupal\user_logout_confirm\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
use Drupal\user_logout_confirm\Form\UserLogoutConfirmForm;

/**
 * Alters Drupal's core logout route so that a confirm is shown.
 */
class UserLogoutConfirmRouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  public function alterRoutes(RouteCollection $collection) {
    // Get the route object for a user to log out.
    if ($route = $collection->get('user.logout')) {
      // Retrieve the current set of defaults.
      $defaults = $route->getDefaults();

      // Indicate to use our confirmation form.
      $defaults['_form'] = UserLogoutConfirmForm::class;

      // Prevent Drupal's core controller being used.
      unset($defaults['_controller']);

      // Set the new values to the defaults section of the route.
      $route->setDefaults($defaults);
    }
  }

}

In order for the UserLogoutConfirmRouteSubscriber to be picked by Drupal, a service has to be added containing the event_subscriber tag. This ensures that our alterRoutes() function gets invoked when Drupal constructs its routing registry (e.g., when a cache rebuild is performed).

services:
  user_logout_confirm.route_subscriber:
    class: Drupal\user_logout_confirm\Routing\UserLogoutConfirmRouteSubscriber
    tags:
      - { name: event_subscriber }

To complete our example, we have added the implementation of the UserLogoutConfirmForm below. When navigating to the user.logout route, the confirmation form is shown, which happens when pressing the logout button and also when custom code redirects the user and uses the route user.logout. When the user submits the form (i.e., confirms to logout), we invoke the logout procedure of the original controller (loaded using Drupal's class resolver service).

<?php

namespace Drupal\user_logout_confirm\Form;

use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Defines a confirmation form when a user logs out.
 */
class UserLogoutConfirmForm extends ConfirmFormBase {

  /**
   * Class resolver service to load in classes using dependency injection.
   *
   * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
   */
  protected $classResolver;

  /**
   * Constructs a new UserLogoutConfirmForm object.
   *
   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $classResolver
   *   The class resolver service.
   */
  public function __construct(ClassResolverInterface $classResolver) {
    $this->classResolver = $classResolver;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('class_resolver')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return "user_logout_confirm_form";
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Get the controller that would normally execute the logout procedure.
    $userController = $this->classResolver->getInstanceFromDefinition('\Drupal\user\Controller\UserController');

    // Initiate the logout procedure that would normally be triggered by the
    // original controller.
    $userController->logout();

    // Redirect the user to the front page.
    $form_state->setRedirect('<front>');
  }

  /**
   * {@inheritdoc}
   */
  public function getCancelUrl() {
    // Redirect the user to the front page.
    return Url::fromRoute('<front>');
  }

  /**
   * {@inheritdoc}
   */
  public function getQuestion() {
    return $this->t('Are you sure you want to logout?');
  }

  /**
   * {@inheritdoc}
   */
  public function getDescription() {
    return '';
  }

}
Category
Drupal
Form
ConfirmForm
Services
RouteSubscriber
ClassResolver
Dependency Injection