Recently I found myself needing to manage cookie data in Drupal 9 for the first time. As oft, my first step in the process was to search through Drupal core and the Drupal API for examples of “the right way” to handle cookies. After I didn’t find any reasonable existing solution within Drupal, it was time to solve this on my own.
In my case I had an array of data I needed to store in a cookie, and that data needed to be easily accessible by many parts of the system. This data (visitor location) determined how various components would appear (or not appear) throughout the site, and what pages the visitor would be recommended.
Problems & Considerations
- We don’t want to access the global data directly through
$_COOKIE
because this is equivalent to changing values with$GLOBALS
. Meaning we don’t have control over the data, we can’t inject it as a dependency, and as more parts of the system use the$_COOKIE
data directly the more those parts become implicitly dependent on each other and on an unreliable statefulness. - Cookies are a part of both the Request and Response. Their current values are submitted to the system as a part of a Request made by the visitors. And the setting of new values for the cookie is a part of the Response the system returns to the visitor.
- Drupal’s legacy cookie functions (
user_cookie_save()
anduser_cookie_delete()
) are incompatible with symfony cookie management because of how the Drupal functions prefix the cookie names. Trust me.
The Solution: Cookies as Services
The first problem with using Drupal’s legacy cookie functions is easily solved by avoiding those functions completely. Let’s forget about those functions forever.
The next consideration is that cookie data is global data. We want to create a reliable API for this data, and that API should be a real citizen of the system. It should be accessible by other parts of the system, and the modification of this data should be easy and reliable. This sounds like a Service to me.
Finally, cookies are a part of the Request and Response flow of the system, so we need to be able to integrate our cookie data into this flow. In the Symfony http foundation, the Request and Response are dispatched as events. And also in Symfony, events are handled as Services. So by making our new Cookie API into a Service we can easily have access to these events.
My Simple Cookie Service
Getting started, we need to know how to do a few things:
- Register a new service and subscribe to events
- Get cookie values from the Request
- Set new cookie values as a part of the Response
How to get a cookie value from Symfony Request
If we have an instance of a Symfony Request, retrieving the value of a specific cookie (named my_custom_cookie
) works by getting the cookie by name from the Request’s cookie bag:
How to set a new cookie value in a Symfony Response
If we have access to an instance of a Symfony Reponse object, setting a new value for a cookie (named my_custom_cookie
) works by adding a new Cookie instance to the Response headers:
Cookie Service Basics
Now that we know how to get and set cookies from Symfony Requests and Responses, let’s lay the foundation for a new service that handles these basics. This service will live in an imaginary Drupal module we’ll call my_cookies
.
There we have it. A very simple module info file, a custom class that will be used to manage our cookie, and a services file that registers our new class as a service with events.
Notice:
- We have injected the
@request_stack
core service and retreived the current request. Now we have a way to get the current cookie’s value. - We have subscribed to the
Symfony\Component\HttpKernel\KernelEvents::RESPONSE
event. With access to the Response we have a way to set a new cookie value.
We’re almost ready to build this service into something practical, but we have a few more problems to solve.
Remaining Problems
- We need to make it easy for other services to get and set the cookie’s value.
- When the
onResponse()
event occurs, we need a way to know whether the cookie should be updated. - We need a way to know whether the cookie should be deleted. And we need method that allows other parts of the system to tell our service to delete the cookie.
- Once we start accessing and modifying the cookie, it would be nice to centralize the cookie’s name so that we don’t have to continuously type it out.
Let’s improve our service by adding some properties and methods that solve each of these problems.
- First, we’ll store the cookie name as a property and create a getter method to standardize access to the cookie name. For this example we don’t need a setter method for the cookie name, because cookies are global data and we don’t want another part of the system to change the name of the cookie this service is managing.
- Also we’ll create a property to track the cookie’s new value, a method for getting the cookie’s value, another method for setting the cookie’s value, and a property where we’ll track whether or not the cookie should be updated during our
onResponse()
event. - During the response event, we need to be able to set our new cookie value.
- Also during the response event we need to be able to delete our cookie if desired.
Result: Reliable Cookie Service
After solving our remaining problems, here is the result. This new service is ready to be used by the system to reliably manage a cookie.
Now let’s look at how another part of the system can make use of our new service.
Example using our new Cookie Service
Let’s assume we have another module (named some_other_module
). Let’s also imagine that this module has its own service that needs to access and modify this cookie, and that its goal is to change the cookie’s value to a random number on each Request. Sure, it’s an impractical example, but the purpose is to show another part of the system can modify our cookie value using the my_simple_cookie
service.
Since our cookie is a service, we can now inject it into any other service.
There we have it. We have a working cookie service, and an example for using that cookie service in another part of the system!
Conclusion
With cookies as services we have made our custom global data into a real citizen of the system. Access to its data is available to the rest of Drupal (or Symfony), controlled, and documented within our service. So what’s next?
There are a number of ways we could improve this class to make it more reusable. Most of the logic contained in this example would apply to any cookie you may want to handle, so there isn’t a reason we shouldn’t be able to reuse this class for multiple cookies. Additionally some cookie data may not as simple as a string or integer, what if we want to store complex data within a cookie? In that case we would need to serialize the data before saving it, and ideally unserialize the data when accessed through the service.
For examples of how we could make this class more reusable, see the cookie_services module in my collection of Drupal 8 examples on GitHub. It contains the following examples:
- Simple Cookie Service that tracks the number of requests a visitor makes to the website.
- Cookie Service for Complex Data that un/serialized the cookie value as json if needed, and allows for the
$cookieName
property to be set so that the class is reusable as multiple services. - Provides an example of using data from the complex cookie as a custom Cache Context.
As far as I can tell this is a new approach to handling cookies, so I’d love to hear what you think about it. Please leave a comment below if you like/dislike this approach, or have thoughts on additional improvements and considerations. Thanks!
Discussion
Hello! Shame your articel have so few comments! Trying to fix that.
It is great job to describe the flow of development like you did that. I just had googled a module for handling cookies but found a gold – the straight guide to write my own modules for everything with simple description of every stage of development, with every why and how. So lack of it in offiicial documentation of Drupal! Thank you so much, sir!
Thanks Dmitriy, I really appreciate that!
Thank you very much for the detailed explanation of how-to and thought process behind it. After banging head against the wall with Drupal documentation, that is erroneous at times (looking at you, hook_views_data_alter() (whoever reads this comment – click to the implementations of this hook by core modules immediately)), it’s a great relief to get a nice explanation for handling cookies.
Worth mentioning, that caching systems of Drupal are extremely likely to get in your way if you set some cookies as a result of form processing; you need to look into settings files to kill some parts of caching system.
Thanks for the write up and wow.. that’s a ridiculous amount of code to set/read a cookie. Drupal core needs to provide a wrapper around this.