Event Dispatcher and redirects
A common thing you’ll have to do as a module developer is to intercept a given request and redirect it to another page, and often, this will have to be dynamic, depending on the current user or other contextual info. What we have to do in order to achieve this is to subscribe to the kernel.request
event (remember this from the previous chapter?) and then change the response directly. However, before seeing an example of this, let’s take a look at how we can perform a simpler redirect from within a Controller. You know, since we’re on the subject.
Redirecting from a Controller
In this chapter, we have written a Controller that returns a render array. We know from the previous chapter that this is picked up by the theme system and turned into a response. In Chapter 4, Theming, we will go into a bit more detail and see how this process is done. However, this render pipeline can also be bypassed if the Controller returns a response directly. Let’s consider the following example:
return new \Symfony\Component\HttpFoundation\Response('my text');
This will bypass much of that processing and return a blank white page with only the my text
string on it. The Response
class we’re using is from the Symfony HTTP Foundation component.
However, we also have a handy RedirectResponse
class that we can use, and it will redirect the browser to another page:
return new \Symfony\Component\HttpFoundation\ RedirectResponse('/node/1');
The first parameter is the URL where we want to redirect to. Typically, this should be an absolute URL; however, browsers nowadays are smart enough to handle a relative path as well. So, in this case, the Controller will redirect us to that path.
Note
Typically, when returning redirect responses, you’ll want to use a child class of RedirectResponse
. For example, we have the LocalRedirectResponse
and TrustedRedirectResponse
classes, which both extend from SecuredRedirectResponse
. The purpose of these utilities is to ensure that redirects are safe.
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. In these cases, 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. Finally, registering event subscribers is a matter of creating a service tagged with event_subscriber
that implements the interface I mentioned earlier.
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 the service that reflects 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. It’s the user session, basically. Certain things about the user are, however, accessible from the AccountInterface
, such as the ID, 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\RequestEvent; /** * 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\ RequestEvent $event */ public function onRequest(RequestEvent $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. 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. This is how we 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, which means 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 RequestEvent
, which basically wraps the Request
object. If we inspect this class, we can see 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.
This being said, I don’t like many aspects of this implementation. So, let’s fix them.
First, we hardcoded the kernel.request
event name (I did—I 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, 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). Make sure you type-hint the service with the interface it implements: RouteMatchInterface
and set it to the routeMatch
class property. Once that is done, we can do this instead:
public function onRequest(RequestEvent $event) { $route_name = $this->routeMatch->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 path name, 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 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\RouteMatchInterface; use Drupal\Core\Routing\LocalRedirectResponse; use Symfony\Component\HttpKernel\KernelEvents; use Drupal\Core\Url;
Note
Again, for the sake of not overloading you with too much information, I omitted a very important aspect here: caching. So, our redirect works, but not very well. We will fix it when we learn about caching in Chapter 11, Caching.
Dispatching events
Since we have discussed how to subscribe to events in Drupal, we should also take a look at how we can dispatch our own events. After all, the Symfony Event Dispatcher component is one of the principal vectors of extensibility in Drupal.
To demonstrate this, we will create an event to be dispatched whenever our HelloWorldSalutation::getSalutation()
method is called. The purpose is to inform other modules that this has happened and potentially allow them to alter the message that comes out of the configuration object—not really a solid use case, but good enough to demonstrate how we can dispatch events.
The first thing that we will need to do is to create an event class that will be dispatched. It can go into the root of our module’s namespace:
namespace Drupal\hello_world; use Symfony\Contracts\EventDispatcher\Event; /** * Event class to be dispatched from the HelloWorldSalutation service. */ class SalutationEvent extends Event { const EVENT = 'hello_world.salutation_event'; /** * The salutation message. * * @var string */ protected $message; /** * @return mixed */ public function getValue() { return $this->message; } /** * @param mixed $message */ public function setValue($message) { $this->message = $message; } }
The main purpose of this event class is that an instance of it will be used to transport the value of our salutation message. This is why we created the $message
property on the class and added the getter and setter methods. Moreover, we use it to define a constant for the actual name of the event that will be dispatched. Finally, the class extends from the base Event
class that comes with Symfony.
Next, it’s time to inject the Event Dispatcher service into our HelloWorldSalutation
service. We have already injected config.factory
, so we just need to add a new argument to the service definition:
arguments: ['@config.factory', '@event_dispatcher']
Of course, we will also receive it in the constructor and store it as a class property:
/** * @var \Symfony\Component\EventDispatcher\ EventDispatcherInterface */ protected $eventDispatcher; /** * HelloWorldSalutation constructor. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * @param \Symfony\Contracts\EventDispatcher\ EventDispatcherInterface $eventDispatcher */ public function __construct(ConfigFactoryInterface $config_factory, EventDispatcherInterface $eventDispatcher) { $this->configFactory = $config_factory; $this->eventDispatcher = $eventDispatcher; }
We will also have the obligatory use statement for the EventDispatcherInterface
at the top of the file:
use Symfony\Contracts\EventDispatcher\ EventDispatcherInterface;
Now, we can make use of the dispatcher. So, instead of the following code inside the getSalutation()
method:
if ($salutation !== "" && $salutation) { return $salutation; }
We can have the following:
if ($salutation !== "" && $salutation) { $event = new SalutationEvent(); $event->setValue($salutation); $event = $this->eventDispatcher->dispatch($event, SalutationEvent::EVENT); return $event->getValue(); }
So, with the above, we decided that if we are to return a salutation message from the configuration object, we want to inform other modules and allow them to change it. We first create an instance of our Event class and feed it the relevant data (the message). Then, we dispatch the named event and pass the event object along with it. Finally, we get the data from that instance and return it.
Pretty simple, isn’t it? What can subscribers do? It’s very similar to what we saw regarding the example on redirects in the previous section. All a subscriber needs to do is listen for the SalutationEvent::EVENT
event and do something based on that. The main thing that it can do is use the setValue()
method on the received event object to change the salutation message. It can also use the stopPropagation()
method from the base Event
class to inform the Event Dispatcher to no longer trigger other listeners that have subscribed to this event.