Effective Caching with Laravel

When should I be caching?

For those new to caching, it can be a tricky question. Some people might be eager to cache anything and everything. Sometimes caching a full page might be the best approach, but in other cases, you’re better off caching individual elements of the page. For example, you might want to cache parts of products to avoid querying your database constantly for product details that you don’t even need.

Let’s take the schema below as an example of a product structure on an e-commerce store:

id - BIGINT
name - VARCHAR(255)
slug - VARCHAR(255)
vendor - VARCHAR(255)
category_id - BIGINT
description - MEDIUMTEXT
published - TINYINT(1)
main_image_uri - VARCHAR(255)
price - INT
variant_fields - JSON
sku - VARCHAR(255)
barcode - VARCHAR(255)
available_stock - INT
weight - INT
created_at - TIMESTAMP
updated_at - TIMESTAMP
deleted_at - TIMESTAMP

There’s at least 1kb of data per record in that schema, and realistically a product might have more fields, as well as the fact that a store with many products risks the chance of slowing down without smart database design. If you’re displaying a list of products though, you likely won’t want most of these fields, most of those only matter at the point of displaying the full single product view.

There’s a few ways we can tackle this, but one approach is to do the following:

  • Take the fields that matter for a category (name, slug, category_id, published, main_image_uri, price) as well as calculate a boolean for whether the product is in stock (there’s many ways to do this, observers and events are two ways that we’ll visit in another post, that’s also important for ensuring that you keep your cache up to date!)
  • Create an array containing this information – you could go the route of just caching the whole list of products for a category, or you could cache each product individually which could be useful for other parts of your site, such as a basket, etc. – We’ll look at just caching the whole list for the sake of simplicity in this post.
  • Fetch the products back from the cache and put them in a form we can display

So why cache?

Given the above, we could be reading 1kb of data per product from the disk (if not more, and ignoring any optimisations done by the database backend of your choice), first off that could start getting slow if your database isn’t designed properly, but also at scale, that’s using database read capacity where we don’t necessarily need to be. In that case, we’re better off using caching, that means we can store our data (or a subset of it that’s the most important to us) in memory using something such as Redis or Memcached.

This is just one of the many ways to use caching, we can also use it to store short term data. Let’s say you want to store the latest weather for a location for example, you could store the latitude and longitude as the key, with the value being the weather data and set a TTL of 15 minutes to account for the weather model updating. That means, after 15 minutes, that key will automatically be deleted from the cache, and all you need to do is implement a check to see whether that key exists, and if not then fetch the latest weather – if you were to use a third-party service, you save API calls this way.

Caching the data

Alright so you’ve decided how you want to cache your data, but how do we actually do it? Well first of all, you’ll need to configure your cache driver in .env. This page on Laravel’s documentation covers how to do this (as of Laravel 12, which was current at the time of writing).

Once your cache driver is set up, it’s time to actually cache the data. For the sake of this tutorial, we’ll write a method that takes a category ID, queries the Category model, and fetches products through a relation. Once we have a list of products, we’ll load them into our cache.

<?php
use App\Models\Category;
use App\Models\Product;

class CacheManager
{
    public static function cacheProducts(int $categoryId): void
    {
        $category = Category::find($categoryId);
        if (!$category) {
            throw new \Exception('Invalid category ID');
        }

        $products = $category->products;
        $cacheProducts = $products->map(function (Product $item) {
            return [
                'name' => $product->name,
                'slug' => $product->slug,
                'category_id' => $product->category_id,
                'published' => $product->published,
                'main_image_uri' => $product->main_image_uri,
                'price' => $product->price,
                'in_stock' => $product->available_stock > 0
            ];
        });

        // Laravel will serialise the data for us
        \Cache::put('products_' . $categoryId, $cacheProducts->toArray());
    }
}
PHP

Now that we have our cacheProducts method, we simply call it with CacheManager::cacheProducts($id);, easy right?!

Retrieving our products

You’re probably looking at the code above and wondering “Alright, but how do I get my products back out of the cache?!”, so let’s implement that next! All we need to do is extend our CacheManager class with a new fetchProducts method, so let’s do that.

<?php
use App\Models\Category;
use App\Models\Product;

class CacheManager
{
    public static function cacheProducts(int $categoryId): void
    {
        ...
    }

    public static function fetchProducts(int $categoryId): array
    {
        $cachedProducts = \Cache::get('products_' . $categoryId, function () {
            return Product::where('category_id', $categoryId)->get();
        });

        return $cachedProducts;
    }
}
PHP

So what does this do? It’s quite simple, and Laravel gives us a great feature for convenience to make life easy! First of all, we try to fetch our products from the cache using the key we created earlier (products_{category ID}), but this is where a cool feature added to Laravel comes in… If the key doesn’t exist in the cache for whatever reason, we can fall back to fetching our products from the database. To make life easy, we simply query the Product model directly (there’s also no harm in doing this in cacheProducts either) which means if there’s no products returned from the database either, we simply get an empty array.

Because of Laravel’s automatic serialisation/deserialisation of cached data, we don’t need to do any processing, so we can immediately return it, and that’s all there is to it!

Next steps

Now that you’ve got a working cache system for your products, you might be wondering what comes next, and that’s where we can leverage other Laravel features. For example, if a product is updated in the database, you might want to automatically update your cache. This can be achieved with events and observers, which if you want to see how they work, you can check that out here!

Leave a Reply

Your email address will not be published. Required fields are marked *