Drupal 9: The Correct Way To Redirect A Page

Drupal 9: The Correct Way To Redirect A Page

22nd May 2022 - 9 minutes read time

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

It is also possible (although not 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 is generally a bad idea to redirect within a hook. You should be either looking at redirecting in a form submission handler or subscribing to an event and redirecting from there.

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.

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

Permalink

"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

frank kelly (Sun, 05/22/2022 - 20:57)

Permalink

Great article, thanks!

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

FMB (Thu, 05/26/2022 - 13:25)

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]);

 

Joel Steidl (Thu, 05/26/2022 - 16:05)

Permalink

Thanks Joel!

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

Add new comment

The content of this field is kept private and will not be shown publicly.