Key Takeaways
- Drupal 8 introduces a new cache API, which solves many performance problems previously encountered in Drupal 7. The cache API allows for the storage of data that takes a long time to compute, reducing the time and resources required to generate and deliver content.
- The cache API works by storing data in multiple ‘bins’, which map to tables that start with the prefix cache_. Interacting with the cache starts by requesting a cache bin. There are simple class methods for managing cache items, including retrieving, storing, invalidating and removing cache items.
- Cache tags are a new feature of the Cache API in Drupal 8. They are used to identify cache items across multiple bins for proper invalidation. This allows for accurate targeting of multiple cache items that contain data about the same object, page, etc., without having to know the cache IDs.
- A demonstration module is provided for testing the impact of the cache API. This module creates a simple page that makes an external API call and caches its results, then displays the actual time it takes for this to happen, contrasting the external call time vs. the cached version time. The module can be found in a provided git repository.
Drupal 8 comes with many improvements over its predecessor we have grown to both love and hate. Next to prominent systems such as Views in core, configuration management or a useful translation service, there are also less known changes but that are equally important to know and use. One such improvement has been the cache API that solves many performance problems we have in Drupal 7.
In this article, I want to shine a bit of light over the new cache API. To this end, we are going to look at how we can use it in our custom modules as we are encouraged to do so much more in Drupal 8.
Additionally, I have prepared a little demonstration in the shape of a module you can install for testing the impact of the cache API. It’s a simple page that in its rendering logic makes an external API call (to a dummy JSON endpoint) and caches its results. The page then displays the actual time it takes for this to happen, contrasting the external call time vs. the cached version time.
The new cache API
Bins
The new cache API (with the default DatabaseBackend storage) is stored in multiple bins which map to tables that start with the prefix cache_
. When interacting with the cache, we always start by requesting a cache bin:
$cache = \Drupal::cache();
Where $cache
will be an instance of the DatabaseBackend
object that represents the default bin (cache_default
). To request a particular bin we pass in the name in the constructor:
$render_cache = \Drupal::cache('render');
Where $render_cache
will represent the render cache bin (which is new in Drupal 8 and is supposed to improve render performance across the board).
As you can see, we are requesting the cache service statically using the \Drupal
class. If we are working inside classes, it is best practice to inject the service from the container. You can do so by specifying as an argument to your service the relevant cache bin service (such as cache.default
). Here you can get a list of all core services including the ones related to cache.
But for the sake of brevity, we will use it statically here.
Retrieving cached items
Once we know which bin we want to work with (for custom modules this will usually be the default bin), we can retrieve and store cache items.
$cache = \Drupal::cache()->get('my_value');
It’s that simple. $cache
will be a stdClass
object containing some metadata about the cache item plus the actual data available under the $cache->data
property. The my_value
parameter is the cache ID.
An important thing to keep in mind is that using the get()
method without a second parameter will not return the cache item if it has been invalidated (either programatically or through expiration). Passing the boolean true
as a second parameter will force it to return the data.
Storing cache items
Although storing new items in the cache is just as easy as retrieving them, we have more options when doing so. To store an item we use the set()
method (instead of get()
like before), a method that takes 2 mandatory parameters and 2 optional ones:
- the cache ID (the string by which we can later reference the item)
- the data (a PHP value such as a string, array or object that gets serialised automatically and stored in the table – should not be over 1MB in size)
- the expiration time (a timestamp in the future when this cache item will automatically become invalid or
-1
which basically means this item never expires. It is best practice to use theDrupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT
constant to represent this value) - tags (an array of cache tags this item can be later identified by)
As an example:
Drupal::cache()->set('my_value', $my_object, CacheBackendInterface::CACHE_PERMANENT, array('my_first_tag', 'my_second_tag'));
This will set a permanent cache item tagged with 2 tags and store a serialised version of $my_object
as the data.
Cache invalidation and removal
Cache invalidation means that the respective items are no longer fresh and are unreliable in terms of what data they hold. They will be removed at the next garbage collection which can also be called using the garbageCollection()
method on the CacheBackend object.
As mentioned above, when storing a cache item we can specify an expiration time. When this time lapses, the cache item becomes invalid but still exists in the bin and can be retrieved. However, we can also invalidate items manually using the invalidate()
, invalidateMultiple()
or invalidateAll()
methods on the CacheBackend object.
Removing items altogether can be done using the delete()
, deleteMultiple()
or deleteAll()
methods. These actions also happen only on the bin the CacheBackend is wrapping and completely remove the respective table records.
Cache tags
Another cool new feature of the Cache API in Drupal 8 are the cache tags (the fourth parameter in the setter method). The role of the tags is to identify cache items across multiple bins for proper invalidation. The purpose is the ability to accurately target multiple cache items that contain data about the same object, page, etc. For example, nodes can appear both on a page and in a view (stored in different cache items in different bins but both tagged with the same node:nid
formatted tag). This allows invalidating both cache items when changes happen to that node without having to know the cache ids.
To manually invalidate caches using the tags, we can use the invalidateTags()
method statically on the \Drupal\Core\Cache\Cache
class:
\Drupal\Core\Cache\Cache::invalidateTags(array('node:5', 'my_tag'));
This will call the cache invalidator service and invalidate all the cache items tagged with node:5
and my_tag
.
Additionally, for Drupal entities we don’t have to create our own tags but can retrieve them from the entity system:
\Drupal\Core\Entity\EntityInterface::getCacheTags()
\Drupal\Core\Entity\EntityTypeInterface::getListCacheTags()
This keeps the tags for Drupal entities consistent across the board.
Demonstrating the cache API
As I mentioned before, I created a small module that allows us to see the benefits of caching data. You can find the module in this git repository but here is the crux of it:
Please note that in this example I access the cache backend service statically to save some space. For a dependency injection approach (the correct approach), take a look at the repository code.
A route file that adds a new route to the /cache-demo
path:
cache_demo_page:
path: 'cache-demo'
defaults:
_controller: '\Drupal\cache_demo\Controller\CacheDemoController::index'
_title: 'Cache demo'
requirements:
_permission: 'access content'
And the controller class that returns the page inside src/Controller/CacheDemoController.php
:
<?php
/**
* @file
* Contains \Drupal\cache_demo\Controller\CacheDemoController.
*/
namespace Drupal\cache_demo\Controller;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use \GuzzleHttp\Client;
/**
* Cache demo main page.
*/
class CacheDemoController extends ControllerBase {
public function index(Request $request) {
$output = array();
$clear = $request->query->get('clear');
if ($clear) {
$this->clearPosts();
}
if (!$clear) {
$start_time = microtime(TRUE);
$data = $this->loadPosts();
$end_time = microtime(TRUE);
$duration = $end_time - $start_time;
$reload = $data['means'] == 'API' ? 'Reload the page to retrieve the posts from cache and see the difference.' : '';
$output['duration'] = array(
'#type' => 'markup',
'#prefix' => '<div>',
'#suffix' => '</div>',
'#markup' => t('The duration for loading the posts has been @duration ms using the @means. @reload',
array(
'@duration' => number_format($duration * 1000, 2),
'@means' => $data['means'],
'@reload' => $reload
)),
);
}
if ($cache = \Drupal::cache()->get('cache_demo_posts') && $data['means'] == 'cache') {
$url = new Url('cache_demo_page', array(), array('query' => array('clear' => true)));
$output['clear'] = array(
'#type' => 'markup',
'#markup' => $this->l('Clear the cache and try again', $url),
);
}
if (!$cache = \Drupal::cache()->get('cache_demo_posts')) {
$url = new Url('cache_demo_page');
$output['populate'] = array(
'#type' => 'markup',
'#markup' => $this->l('Try loading again to query the API and re-populate the cache', $url),
);
}
return $output;
}
/**
* Loads a bunch of dummy posts from cache or API
* @return array
*/
private function loadPosts() {
if ($cache = \Drupal::cache()->get('cache_demo_posts')) {
return array(
'data' => $cache->data,
'means' => 'cache',
);
}
else {
$guzzle = new Client();
$response = $guzzle->get('http://jsonplaceholder.typicode.com/posts');
$posts = $response->json();
\Drupal::cache()->set('cache_demo_posts', $posts, CacheBackendInterface::CACHE_PERMANENT);
return array(
'data' => $posts,
'means' => 'API',
);
}
}
/**
* Clears the posts from the cache.
*/
function clearPosts() {
if ($cache = \Drupal::cache()->get('cache_demo_posts')) {
\Drupal::cache()->delete('cache_demo_posts');
drupal_set_message('Posts have been removed from cache.', 'status');
}
else {
drupal_set_message('No posts in cache.', 'error');
}
}
}
Inside the index()
method we do a quick check to see whether the clear
query parameter is present in the url and call the clearPosts()
method responsible for deleting the cache item. If there isn’t one, we calculate how long it takes for the loadPosts()
method to return its value (which can be either the posts from the cache or from the API). We use Guzzle to make the API call and when we do, we also store the results directly. Then we just output the duration of the call in milliseconds and print 2 different links depending on whether there is cache stored or not (to allow us to clear the cache item and run the API call again).
When you navigate to cache-demo
for the first time, the API call gets made and the 100 posts get stored in the cache. You can then reload the page to see how long it takes for those posts to be retrieved from the cache. Upon doing that, you’ll have a link to clear the cache (by a page refresh with the clear
query string) followed by another link which refreshes the page without the clear
query string and that in turn makes the API call again. And on like that to test the contrast in duration.
Conclusion
In this article we’ve looked at how easy it is to use the Cache API in Drupal 8. There are some very simple class methods that we can use to manage cache items and it has become too straightforward for us not to start using it in our custom modules. I encourage you to check it out, play around with the API and see for yourself how easy it is to use.
Frequently Asked Questions (FAQs) about Drupal 8 Cache API
What is the Cache API in Drupal 8?
The Cache API in Drupal 8 is a powerful feature that allows developers to store data that takes a long time to compute. It is designed to reduce the time and resources required to generate and deliver content. The Cache API stores the results of these expensive computations and serves them from the cache when the same result is needed again, thereby improving the performance of the website.
How does the Cache API work in Drupal 8?
The Cache API in Drupal 8 works by storing data in a cache bin. A cache bin is a logical grouping of cached items. When a piece of data is cached, it is stored in a specific cache bin. The data can then be retrieved from the cache bin when needed, instead of being computed or fetched again. This significantly reduces the load on the server and improves the speed of the website.
How can I use the Cache API in Drupal 8?
To use the Cache API in Drupal 8, you need to use the cache service. The cache service provides methods for storing, retrieving, and invalidating cached data. You can use the cache service in your custom modules or themes to improve the performance of your website.
What are cache tags in Drupal 8?
Cache tags in Drupal 8 are a way to invalidate cached data. When you cache data, you can add one or more cache tags to it. When the data associated with a cache tag changes, you can invalidate all cached data with that tag. This ensures that your website always serves the most up-to-date data.
How can I use cache tags in Drupal 8?
To use cache tags in Drupal 8, you need to add them to your cached data. You can do this by using the addCacheTags method of the render array. When the data associated with a cache tag changes, you can use the invalidateTags method of the cache service to invalidate all cached data with that tag.
What are cache contexts in Drupal 8?
Cache contexts in Drupal 8 are a way to vary cached data. They allow you to cache different versions of the same data for different situations. For example, you can use cache contexts to cache different versions of a block for different users, languages, or URLs.
How can I use cache contexts in Drupal 8?
To use cache contexts in Drupal 8, you need to add them to your cached data. You can do this by using the addCacheContexts method of the render array. The cache system will then automatically vary the cached data based on the specified cache contexts.
What are cache max-age in Drupal 8?
Cache max-age in Drupal 8 is a way to specify how long cached data should be considered valid. After the specified time has passed, the cached data will be invalidated and regenerated.
How can I use cache max-age in Drupal 8?
To use cache max-age in Drupal 8, you need to set it in your cached data. You can do this by using the setCacheMaxAge method of the render array. The cache system will then automatically invalidate the cached data after the specified time has passed.
How can I clear the cache in Drupal 8?
To clear the cache in Drupal 8, you can use the clear all caches option in the administration menu. You can also use the cache rebuild command in Drush, which is a command line tool for Drupal. Additionally, you can programmatically clear specific cache bins or invalidate specific cache tags by using the cache service.
Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.