So far in this chapter, we've mostly preoccupied ourselves with render arrays and how we can expose them to the Cache API for better performance. It's now time to talk a bit about how cache entries are stored by default in Drupal and how we can interact with them ourselves in our code.
As mentioned earlier, a central interface for the cache system is the CacheBackendInterface, which is the interface any caching system needs to implement. It basically provides the methods for creating, reading, and invalidating cache entries.
As we might expect, when we want to interact with the Cache API, we use a service to retrieve an instance of the CacheBackendInterface. However, the service name we use depends on the cache bin we want to work with. Cache bins are repositories that group together cache entries based on their type. So, the aforementioned implementation wraps a single cache bin, and each bin has a machine name. The service name will then be in the following format: cache.[bin]. This means that for each cache bin, we have a separate service.
The static shorthand for getting this service looks like this:
$cache = \Drupal::cache();
This will return the default bin represented by a CacheBackendInterface implementation. If we want to request a specific bin, we pass the name as an argument:
$cache = \Drupal::cache('render');
This will return the render cache bin.
And of course, if we need to inject a cache bin wrapper somewhere, we simply use the service machine name in the format I mentioned before.
Even though we have a separate service for each cache bin, they all basically do the same thing, and that is use the CacheFactory to instantiate the right type of cache backend for that bin. Individual cache backends can be registered and set as the default either globally or for specific bins.
As I mentioned at the beginning of the chapter, the default cache backend in Drupal—the one this factory will instantiate for all the bins—is the DatabaseBackend. Each bin is represented by a database table. This is similar in concept to what we had in Drupal 7.
Now that we know how to load the cache backend service, let's see how we can use it to read and cache things. When it comes to this, your number one reference point is the CacheBackendInterface which documents all the methods. However, since it does not reinforce return values, the examples we will see next are done with the database cache backend. They might differ from other cache backend implementations.
The first method we'll talk about is get(), which takes the ID of the cache entry we want to retrieve ($cid) and an optional $allow_invalid parameter. The first parameter is clear enough, but the second one is used in case we want to retrieve the entry even if it has expired or has been invalidated. This can be useful in those cases in which stale data is preferred over the recalculation costs of multiple concurrent requests:
$data = $cache->get('my_cache_entry_cid');
The resulting $data variable is a PHP standard class that contains the data key (the data that has been cached) and all sorts of metadata about the cache entry: expiration, creation timestamp, tags, valid status, and so on.
Of course, there is also a getMultiple() method which you can use to retrieve multiple entries at once.
More fun, though, is the set() method which allows us to store something in the cache. There are four parameters to this method:
- $cid : The cache ID that can be used to retrieve the entry.
- $data : A serializable data structure such as an array or object (or simple scalar value).
- $expire : The UNIX timestamp after which this entry is considered invalid, or CacheBackendInterface::CACHE_PERMANENT to indicate that this entry is never invalid unless specifically invalidated. The latter is the default.
- $tags : An array of cache tags that will be used to invalidate this entry if it depends on something else (cache metadata, basically).
So to use it, we would do something like this:
$cache->set('my_cache_entry_cid', 'my_value');
With this statement we are creating a simple non-serialized cache entry into our chosen bin that does not expire unless specifically invalidated (or deleted). Subsequent calls with the same cache ID will simply override the entry. If the cache value is an array or object, it will get serialized automatically.
When it comes to deleting, there are two easy methods: delete() and deleteMultiple(), which take the $cid (or an array of cache IDs, respectively) as an argument and removes the entries from the bin completely. If we want to delete all the items in the bin, we can use the deleteAll() method.
Instead of deleting entries, quite often it's a good idea to invalidate them. We'll still be able to retrieve the data using the $allow_invalid parameter and can use the entry while the new one is being recalculated. This can be done almost exactly as deleting but using the following methods instead: invalidate(), invalidateMultiple(), and invalidateAll().
OK, but what about those cache tags we can store with the entry? We already kind of know their purpose and that is to tag cache entries across multiple bins with certain data markers that can make them easy to invalidate when the data changes. Just like with render arrays. So, how can we do this?
Let's assume that we store the following cache entry:
$cache->set('my_cache_entry_cid', 'my_value', CacheBackendInterface::CACHE_PERMANENT, ['node:10']);
We essentially make it dependent on changes to the Node with the ID of 10. This means that when that node changes, our entry (together with all other entries in all other bins that have the same tag) becomes invalid. Simple as that.
But we can also have our own tags that make it depend on something custom of ours like a data value (which, as we discussed earlier in the chapter, should implement the CacheableDependencyInterface) or a process of some kind. In that case, we would also have to take care of invalidating all the cache entries that have our tag. The simplest way we can do this is statically, using the Cache class we encountered earlier when merging metadata together:
Cache::invalidateTags(['my_custom_tag']);
This will invalidate all cache entries that are tagged with any of the tags passed in the array. Under the hood, this method uses a static call to the cache invalidator service, so whenever possible, it's best to actually inject that service—cache_tags.invalidator.