Drupal 9: The Correct Way To Redirect A Page

I was recently working with a Drupal 9 site and found some strange behaviour when saving certain types of content. The page would appear to save, but much of the content appeared to be missing. It had even failed to generate the paths for the page based on the path auto rules.

Digging deeper I found the root cause of the problem was an improperly created redirect in an insert hook within custom code written on the site. This code looked for the creation of a particular content type and then forced the redirect to happen.

This was more or less the code in question. 

use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;

function mymodule_entity_insert(EntityInterface $entity) {
  if ($entity->getType() == 'article') {
    (new RedirectResponse('/node/' . ($entity->id())))->send();
    exit();
  }
}

This isn't great code in itself, but the worst part is the call to the exit() function. This stops any code execution straight away, which is what caused out content type to be half created. By stopping code execution like this we are preventing any other hooks or services from acting upon the content type being inserted.

The secondary effect of this is that it also bypasses Drupal's shutdown functions. When Drupal starts a page request it registers a few methods that are to be called as the page response is closed down. This includes session management methods, but can also include a cron handler (if cron has been configured like that).

There is probably a reason why the exit() function was added. by default Drupal will perform a redirect after creating a page and I think the original developer was probably trying to prevent the upstream redirect from taking place. Unfortunately, they ended up creating more problems than just a redirect issue.

Seeing this code got me to think about how to perform a redirect without causing problems in Drupal. The answer isn't that straightforward as it depends on what context you are dealing with at the time. I thought it would be useful to show how to redirect a Drupal page depending on where the code is being written.

Redirecting In Controllers

Redirecting in a controller is perhaps the easiest thing to do as you just need to return a new Symfony\Component\HttpFoundation\RedirectResponse object. The redirect object accepts a string pointing to a page, and this can be created easily using a Drupal\Core\Url object.

$url = Url::fromRoute('entity.node.canonical', ['node' => 1]);
return new RedirectResponse($url->toString());

Drupal will see this object type being returned from the controller action and redirect the page accordingly. This redirect takes place after it has finished processing the upstream code.

Redirecting In Forms

If you want to redirect a form submission when you need to use the setRedirect() method on the form state object during the form submission handler. This method takes the route you want to redirect to and can be used like this.

$form_state->setRedirect('entity.node.canonical', ['node' => 1]);

Alternatively, you can use the setRedirectUrl() method of the form state, which takes in a Drupal\Core\Url object as the argument.

$url = Url::fromRoute('entity.node.canonical', ['node' => 1]);
$form_state->setRedirectUrl($url);

The only caveat to this method is that you must not also use the setRebuild() method on the form state as this will cause the redirect to not be processed.

The above examples assume that you are writing a form class, but what if you wanted to perform a redirect on form you don't have control over? By using a hook_form_alter() hook we can inject a form submission handler into the form state and then add the redirection code to that handler.

Here is the hook_form_alter() hook.

function mymodule_form_alter(&$form, FormStateInterface $form_state, $form_id) {
    if ($form_id === 'node_article_form') {
    $form['actions']['submit']['#submit'][] = 'mymodule_article_form_submit';
  } 
}

The submission handler would look something like this.

function mymodule_article_form_submit($form, FormStateInterface $form_state) {
  $form_state->setRedirect('entity.node.canonical', ['node' => 1]);
}

In my opinion, this is how the original offending code should have been written. By adding a form alter hook to the form and injecting a custom submission handler the redirect can be easily added to the form state, which can then be processed by the form submission handler. The redirect would then take place and it wouldn't have had to battle with any other redirects that Drupal had created.

Redirecting In Events

If you are in an Event then the approach is slightly different again. Here, you need to inject the RedirectResponse into the Event object using the setResponse() method. The following code will redirect if a condition has been met.

class MyEventSubscriber implements EventSubscriberInterface {

  public static function getSubscribedEvents() {
    $events['kernel.request'] = ['someEvent', 28];
    return $events;
  }

  private function someEvent(KernelEvent $event) {
    if (/* some condition */) {
      $url = Url::fromRoute('entity.node.canonical', ['node' => 1]);
      $response = new RedirectResponse($url->toString());
      $event->setResponse($response);
    }
  }
}

The creation of the RedirectResponse object is exactly the same as usual. In this case, Drupal will perform the redirect once the event has been processed. Make sure you include your redirect in a condition (especially when responding to the kernel.request event) as this would otherwise lead to an infinite redirection loop.

Be warned that other events can also be triggered here and might override or change the response. In which case you need to change your event subscriber weight to ensure it occurs last in the event processing.

Redirecting In Hooks And Services

These are the preferred methods of redirecting in Drupal, but you can also redirect in hooks and services.

Redirecting In A Service

If you have a service that performs an action and you want to push the user to a particular page then you can do this with the RedirectResponse() object. Just create an object using a URL string and call the send() method to trigger the redirect.

<?php

namespace Drupal\my_module;

class MyService {

  public function doTheThing($entity) {
    $url = Url::fromRoute('entity.node.canonical', ['node' => $entity->id()]);
    $redirect = new RedirectResponse($url->toString());
    $redirect->send();
  }

}

Note that send() will send the headers and content and then trigger a flush of your output buffers so this method should be used with caution. This isn't an ideal way to perform a redirect as it can interrupt the flow of other actions. It can, however, help with the separation of concerns when you need to perform a redirect within your module.

Redirecting In Hooks

It is also possible (although not generally recommended) to perform a redirect in a hook. It is best practice to generate a Url object from a route (rather than to hard code the node path into the redirect) as this will allow for the path to change in the future.

use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RedirectResponse;

function mymodule_node_insert(EntityInterface $entity) {
  if ($entity->getType() == 'article') {
    $url = Url::fromRoute('entity.node.canonical', ['node' => 1]);
    $redirect = new RedirectResponse($url->toString());
    $redirect->send();
  }
}

It's interesting to note that Redirect module, which is designed around redirection, uses an event handler to create and return the redirect within an event object. Although the module contains a few hooks, none of them deal with the redirection of content.

Don't Redirect Using Hooks And Services

It is generally a bad idea to redirect within a hook or a service. The reason being that you can interrupt the flow of the page request for other parts of your site. You should be either looking at redirecting in a form submission handler or subscribing to an event and redirecting from there.

If you want to redirect a form submission for a form that you don't have control over then you'll need to use a hook_form_alter() to alter the form to add a redirection call to the submission of the form.

Depending on the type of form being altered will dictate how you add the submission handler to your form.

/**
 * Implements hook_form_alter().
 */
function mymodule_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  if ($form_id == 'some_form') {
    // Pick _one_ of the following submit additions, which will depend on the type
    // of form being altered.

    // Some forms have a "simple" submit hanlder that can be referenced like this.
    $form['#submit'][] = '_mymodule_form_submit_handler';

    // Other forms have an actions section, which must be updated to include
    // the new submit hanlder.
    $form['actions']['submit']['#submit'][] = '_mymodule_form_submit_handler';
  }
}

The "simple" variant tends to be in configuration forms or other single page forms whereas action submission handlers tend to be on entity forms and more complex form. It depends on how the form is build, but the general rule of thumb is that if you've added the submit handler to the form and it isn't called then swap to the other variant.

With this in place you can set the form redirect through the submit handler we added before.

/**
 * Form submission handler that redirects the form.
 *
 * @param array $form
 *   The form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state.
 */
function _mymodule_form_submit_handler($form, FormStateInterface $form_state) {
  // Set redirect to homepage form.
  $form_state->setRedirect('some_route');
}

This method of redirect will allow the form to complete any tasks it needs before redirecting, which prevents nasty bugs if the form is redirected too early in the processing of submit handlers. It should be noted that your redirect can be changed by other hooks so that is something to be careful of when implementing this code.

I have used the \Drupal\Core\Url class a lot in this article. If you want to know more about the Url class then you can read an article about it that goes into detail on how to generate URLs and links in Drupal.

As a final note. Don't, whatever you do, add functions like die() or exit() to your Drupal code or you will break Drupal in interesting ways. Drupal needs to finish running the code for the page correctly and adding functions like this will cause Drupal to break in interesting ways.

Comments

"When Drupal starts a page request is registers a few methods that are to be called as the page response is closed down"

should be "it registers"

obvious enough but requires a double-take at first

Permalink

Corrected it.

Thank you frank, I appreciate you feedback :)

Name
Philip Norton
Permalink

Great article, thanks!

If you want to redirect after login, the best way is to use a middleware.

Permalink

Thanks for the post. One piece of feedback. For controllers, I've done something like this: 

 

return $this->redirect('entity.node.canonical', ['node' => $nid]);

 

Permalink

Thanks Joel!

I had missed the redirect() method. Thanks for letting me know about it.

Name
Philip Norton
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
1 + 2 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.