RouteSubscriber: serve a different page on an existing URL

Often when already existing pages/forms have to be changed, you 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 not the solution you want to implement. Luckily, a different solution feels far more correct: Drupal's RouteSubscriber.

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 usually 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 logout procedure is triggered when the user navigates to user/logout, e.g., when clicking on the logout 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);
    }
  }

}

For the UserLogoutConfirmRouteSubscriber to be picked by Drupal, a service has to be added containing the event_subscriber tag. This service 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 Drupal\user\Controller\UserController;
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(UserController::class);

    // 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

Comments

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.