Redirecting from a subscriber

Many times, our business logic dictates that we need to perform a redirect from a certain page to another if various conditions match. For these, we can subscribe to the request event and simply change the response, essentially bypassing the normal process, which would have gone through all the layers of Drupal. However, before we see an example, let's talk about the Event Dispatcher for just a bit.

The central player in this system is the event_dispatcher service, which is an instance of the ContainerAwareEventDispatcher class. This service allows the dispatching of named events that take a payload in the form of an Event object, which wraps the data that needs to be passed around. Typically, when dispatching events, you'll create an Event subclass with some handy methods for accessing the data that needs to be passed around. Finally, instances of EventSubscriberInterface listen to events that have certain names and can alter the Event object that has been passed. Essentially, then, this system allows subscribers to change data before the business logic uses it for something. In this respect, it is a prime example of an extension point in Drupal 8. Finally, registering event subscribers is a matter of creating a service tagged with event_subscriber and that implements the interface.

Let's now take a look at an example event subscriber that listens to the kernel.request event and redirects to the home page if a user with a certain role tries to access our Hello World page. This will demonstrate both how to subscribe to events and how to perform a redirect. It will also show us how to use the current route match service to inspect the current route.

Let's create this subscriber by first writing the service definition for it:

hello_world.redirect_subscriber:
class: \Drupal\hello_world\EventSubscriber\HelloWorldRedirectSubscriber
arguments: ['@current_user']
tags:
- { name: event_subscriber }

As you can see, we have the regular service definition with one argument and with the event_subscriber tag. The dependency is actually the service that points to the current user (either logged in or anonymous) in the form of an AccountProxyInterface. This is a wrapper to the AccountInterface, which represents the actual current user. Also, when I say user, I mean an object that has certain data about the user and not the actual entity object with all the field data (the user session basically). Certain things about the user are, however, accessible from the AccountInterface, such as the ID, the name, roles, and email. I recommend that you check out the interface for more info. However, for our example, we will use it to check whether the user has the non_grata role, which will trigger the redirect I mentioned.

Next, let's look at the event subscriber class itself:

namespace Drupal\hello_world\EventSubscriber;

use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Subscribes to the Kernel Request event and redirects to the homepage
* when the user has the "non_grata" role.
*/
class HelloWorldRedirectSubscriber implements EventSubscriberInterface {

/**
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;

/**
* HelloWorldRedirectSubscriber constructor.
*
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
*/
public function __construct(AccountProxyInterface $currentUser) {
$this->currentUser = $currentUser;
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events['kernel.request'][] = ['onRequest', 0];
return $events;
}

/**
* Handler for the kernel request event.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function onRequest(GetResponseEvent $event) {
$request = $event->getRequest();
$path = $request->getPathInfo();
if ($path !== '/hello') {
return;
}

$roles = $this->currentUser->getRoles();
if (in_array('non_grata', $roles)) {
$event->setResponse(new RedirectResponse('/'));
}
}
}

As expected, we store the current user as a class property so that we can use it later on. Then, we implement the EventSubscriberInterface::getSubscribedEvents() method. This method needs to return a multidimensional array, which is basically a mapping between event names and the class methods to be called if that event is intercepted. And this is how we actually register methods to listen to one event or another, and we can listen to multiple events in the same subscriber class if we want. It's typically a good idea to separate these, however, into different, more topical, classes. The callback method name is inside an array whose second value represents the priority of this callback compared to others you or other modules may define. The higher the number, the higher the priority, the earlier in the process it will run. Do check the documentation on the interface itself for a good description of the ways you can subscribe to events.

In our example, we listen to the kernel.request event I mentioned in the previous chapter. This event is dispatched by Symfony's HttpKernel, passing an instance of GetResponseEvent, which basically wraps the Request object. The name of the Event class usually well describes the purpose of the event. In this case it is looking for a Response object to deliver to the browser. If we inspect the class, we can note that it has a setResponse() method on it, which we can use to set the response. If a subscriber provides one, it stops the event propagation (none of the other listeners with a lower priority are given a chance) and the response is returned.

So, in our onRequest() callback method, we check the current path being requested, and if it is ours and the current user has the non_grata role, we set the RedirectResponse onto the event to redirect it to the home page. This will do the job we set out to do. If you go to the /hello page as a user with that role, you should be redirected to the home page. That being said, I don't like many aspects about this implementation. So, let's fix them.

First, we hardcoded the kernel.request event name (I did, can't blame you for that). Any decent code that dispatches events will use a class constant to define the event name and the subscribers should also reference that constant. Symfony has the KernelEvents class just for that purpose. Check it out and see what other events are dispatched by the HttpKernel, as they are all referenced there.

So, instead of hardcoding the string, we can have this:

$events[KernelEvents::REQUEST][] = ['onRequest', 0];

Second, the way we do the path handling in the onRequest() method is all sorts of wrong. We are hardcoding the /hello path in this condition. What if we change the route path because our boss wants the path to be /greeting? I also don't like the way we passed the path to the RedirectResponse. The same thing applies (although in the case of the home page, not so much): what if the path we want to redirect to changes? Let's fix these problems using routes instead of paths. They are system-specific and are unlikely to change because of business requirements.

The problem is that we are unable to understand which route is being accessed from the Request object. Instead then, we can use the current_route_match service—a very popular one you'll use often—which gives us loads of info about the current route. So, let's inject that into our event subscriber. By now, you should know how to do this on your own (check the final code if you still have trouble). Once that is done, we can do this instead:

public function onRequest(GetResponseEvent $event) {
$route_name = $this->currentRouteMatch->getRouteName();

if ($route_name !== 'hello_world.hello') {
return;
}

$roles = $this->currentUser->getRoles();
if (in_array('non_grata', $roles)) {
$url = Url::fromUri('internal:/');
$event->setResponse(new LocalRedirectResponse($url->toString()));
}
}

From the CurrentRouteMatch service, we can figure out the name of the current route, the entire route object, parameters from the URL, and other useful things. Do check out the class for more info on what you can do, as I guarantee that they will come in handy.

Instead of checking against the pathname, we now check against the route name. So, if we change the path in the route definition, our code will still work. Then, instead of just adding the path to the RedirectResponse, we can build it first using the Url class we learned about in the previous section. Granted, in our example, it is probably overkill, but had we redirected it to a known route, we could have built it based on that, and our code would have been more robust. Additionally, using the Url class, we can also check other things such as access, and its toString() method simply turns it into a string that can be used for the RedirectResponse. Finally, instead of the simple RedirectResponse, we are using the LocalRedirectResponse class instead as we are redirecting to a local (safe) path.

With this, we will get the same redirect, but in a much cleaner and more robust way. Of course, only after adjusting the use statements at the top by removing the one for the RedirectResponse and adding the following:

use Drupal\Core\Routing\CurrentRouteMatch; 
use Drupal\Core\Routing\LocalRedirectResponse; 
use Symfony\Component\HttpKernel\KernelEvents; 
use Drupal\Core\Url;