This article was originally written by Ashley Allen on the Honeybadger Developer Blog.
In the web development world, speed and performance are must-haves. Regardless of whether you're building a website, software-as-a-service (SaaS), or bespoke web software, it's important that it loads quickly to keep your users happy. To help achieve the desired speed, we can use caching.
Caching is a method of storing data inside a "cache" or high-speed storage layer. It's typically (but not always) stored in memory; thus, it's quicker to store and fetch data from it rather than from a database or file storage.
For example, let's imagine that we have some database queries that we need to perform and take a long time or use a lot of memory to run. To reduce this overhead, we could perform the query and then store its results in the cache. This means that the next time we need to run this query, we can grab the results from the cache rather than executing the database query again.
Likewise, we can also use this approach to cache the responses of external API requests. For example, let's imagine that you need to make a request to an exchange-rate API to find the exchange rate between two currencies at a given date and time. We could cache this result after fetching it once so that we don't need to make any future requests if we need this exchange rate again.
Laravel provides a simple yet powerful API for interacting with the cache. By default, Laravel supports caching using
Redis, Memcached, DynamoDB, databases, files, and arrays.
The Benefits of Using Caching
As mentioned above, when used effectively, caching can reduce the workload on your file storage system, database, and application servers. For example, let's imagine that you have a complex database query that is CPU and memory intensive. If this query isn't run very often, it may not be too much of an issue. However, if this query is run regularly, you might start to notice
a performance hit on your application. Resource-heavy queries slow down the given query and can affect other parts of your system for other users.
So, to reduce the infrastructure overhead caused by this query, we could execute it and then store its results in the cache. By doing this, the next time that we need to fetch these results, we'll be able to retrieve them from the cache instead of having to run the database query again.
We can also apply this same logic if we need to run an expensive PHP script or read a file regularly. By caching the output of the script or the contents of the file, we can reduce the application server’s overhead.
Likewise, as mentioned above, we can cache the responses from external API requests so that we don't need to make duplicate queries. By making fewer queries, we can also reduce the chances of hitting a throttle limit or monthly request limit that an API might impose.
As we know, loading times are becoming more important every day, especially now that Google is using loading times as ranking factors. Research suggests that users tend to leave pages that take more than three seconds to load. Let's imagine that we have a database query that takes a few seconds to execute. Now, a few seconds might not seem too bad at first; but if you compound these few seconds with the time taken in other parts of your Laravel app to load your page, you can see how it might start to exceed the three-second threshold.
So, due to how fast data can be retrieved from the cache, we can store the results of the query so that we don't need to execute the time-intensive query again.
Storing Data in the Cache
Now that we've covered the basics of what caching is and the benefits of using it, let's explore how we can use it in our Laravel applications. For the purpose of this article, we will be using extremely simple queries, but they should still explain the overall concepts of caching in Laravel.
Let's say that we have a method that queries the database to fetch all the users that match a given criteria, as shown in this example:
public function getUsers($foo, $bar)
{
return User::query()
->where('foo', $foo)
->where('bar', $bar)
->get();
}
If we wanted to update this method so that it caches the results of this query, we could use the forever()
method,
which will take the results of the user query and then store the result in the cache without an expiry date. The example below shows how we can do this:
public function getUsers($foo, $bar)
{
$users = User::query()
->where('foo', $foo)
->where('bar', $bar)
->get();
Cache::forever("user_query_results_foo_{$foo}_bar_{$bar}", $users);
return $user;
}
A cache is typically a key-value pair store, which means that it has a unique key that corresponds to a value (the actual cached data). Therefore, whenever you try to store anything in the cache or retrieve it, you need to make sure that you are using a unique key so that you don't overwrite any other cached data.
As you can see in the example above, we have appended the $foo
and $bar
arguments to the end of the cache key. Doing this allows us to create a unique cache key for each parameter that might get used.
There may also be times when you want to store your data in the database but with an expiry date. This can be useful if you're caching the results of an expensive query that's used for statistics on a dashboard or a report.
So, if we wanted to cache our users query above for 10 minutes, we could use the put()
method:
public function getUsers($foo, $bar)
{
$users = User::query()
->where('foo', $foo)
->where('bar', $bar)
->get();
Cache::put("user_query_results_foo_{$foo}_bar_{$bar}", $users, now()->addMinutes(10));
return $users;
}
Now, after 10 minutes have passed, the item will expire and won't be able to be retrieved from the cache (unless it has since been re-added).
Fetching Data from the Cache
Let's say that you want to update the users query from the examples above to check whether the result has already been cached. If it has, we can fetch and return it from the cache. Otherwise, we can execute the database query, cache the result for 10 minutes, and then return it. To do this, we could use the following code:
public function getUsers($foo, $bar)
{
$cacheKey = "user_query_results_foo_{$foo}_bar_{$bar}";
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
$users = User::query()
->where('foo', $foo)
->where('bar', $bar)
->get();
Cache::put($cacheKey, $users, now()->addMinutes(10));
return $users;
}
Although this still looks readable, we can use Laravel's remember()
method to further simplify the code and reduce its complexity. This method works in the same way as our example above:
public function getUsers($foo, $bar)
{
return Cache::remember("user_query_results_foo_{$foo}_bar_{$bar}", now()->addMinutes(10), function () use ($foo, $bar) {
return User::query()
->where('foo', $foo)
->where('bar', $bar)
->get();
});
}
Caching Data for the Lifetime of a Request
There may be times when you need to run the same query multiple times in the same request. As a very basic example, let's imagine that you want to cache the user query from our examples above but only for the duration of the request. This way, you can still get the speed benefits and reduced resource usage of caching, but without having to worry that the data will persist across other requests.
To do this, we can use the array
cache driver provided by Laravel. This driver stores the data inside a PHP array in the cache, meaning that once the request has finished running, the cached data will be deleted.
To show how we could cache the user query above for the request lifetime, we could use the following:
public function getUsers($foo, $bar)
{
return Cache::store('array')->remember("user_query_results_foo_{$foo}_bar_{$bar}", function () use ($foo, $bar) {
return User::query()
->where('foo', $foo)
->where('bar', $bar)
->get();
});
}
As you can see in the example, we used store('array')
to choose the array
cache driver.
Using Laravel's Cache Commands
Because of the way that Laravel runs, it boots up the framework and parses the routes file on each request made. This requires reading the file, parsing its contents, and then holding it in a way that your application can use and understand. So, Laravel provides a command that creates a single routes file that can be parsed much faster:
php artisan route:cache
Please note, though, that if you use this command and change your routes, you’ll need to make sure to run the following:
php artisan route:clear
This will remove the cached routes file so that your newer routes can be registered.
Similar to route caching, each time a request is made, Laravel is booted up, and each of the config files in your project are read and parsed. So, to prevent each of the files from needing to be handled, you can run the following command, which will create one cached config file:
php artisan config:cache
Like the route caching above, though, you’ll need to remember to run the following command each time you update your .env file or config files:
php artisan config:clear
Likewise, Laravel also provides two other commands that you can use to cache your views and events so that they are precompiled and ready when a request is made to your Laravel application. To cache the events and views, you can use the following commands:
php artisan event:cache
php artisan view:cache
Like all the other caching commands, though, you'll need to remember to bust these caches whenever you make any changes to your code by running the following commands:
php artisan event:clear
php artisan view:clear
Common Pitfalls of Caching
Although caching can have huge benefits in terms of performance, it can also lead to quite a few problems if it's implemented incorrectly.
For example, if cached data is not busted at the right time, it can lead to outdated and stale data that are incorrect. To contextualize this, let's imagine that our Laravel application has some settings stored in the database and that we fetch these settings on each page load. So, to gain a speed improvement, we decide to cache the settings for 10 minutes. If we forget to bust the cache in our code each time the settings are updated, it could lead to the settings being incorrect for up to 10 minutes, when it's automatically busted.
Further, it can be very easy to increase the complexity of a codebase if caching is used where it's not needed. As shown in the examples above, caching is very easy to implement in Laravel. However, it could be argued that if you don't get too much of a performance gain by caching a specific query or method, you might not need to use it. Adding too much caching in the wrong places can lead to hard-to-track-down bugs and unpredictable behavior. Therefore, it's better to just add it to the places where you get an obvious and clear improvement.
Additionally, if you are adding caching to your Laravel application, it can often mean that you need to add a caching server, such as Redis, to your application's infrastructure. Using this approach could introduce an extra attack vector, as well as open an extra port on the server(s), but if your application's infrastructure security is configured well, it shouldn't be an issue. However, it should definitely be something to remember, especially if you're caching personally identifiable information (PII), because you wouldn't want to risk any cached data being leaked as a result of an attack.
Conclusion
Hopefully, this article has clearly demonstrated what caching is and how you can implement it within your own Laravel applications for performance gains. As you can see, caching can be added to your application really easily thanks to the API provided by Laravel. However, it's important to remember the most common pitfalls when you add it, such as remembering to bust the cache to prevent stale or outdated data.
Top comments (0)