Let's create a DataCache layer for your Laravel apps

Let's create a DataCache layer for your Laravel apps

·

4 min read

Hey guys,

We all know that Cache facade plays a good role across the Laravel application lifecycle, isn't it?

Cache simply boosts up the requests by retaining the computed data from file storage or memory (aka RAM) depending on our configuration.

Today I'm gonna show you an awesome way to maintain your cache flow.

The Problems

Not only for other facades but also Cache provide us with a really simple way to use things (the beauty of Laravel - simple yet elegant)

However, it's a double-edged sword. We code things and we see it's simple, no biggie.

But by the nature of software development, from time to time, the codebase increases, and maintaining becomes harder.

Then people start to blame Laravel. It's not Laravel's fault, right? Laravel doesn't do anything, Laravel allows us to use any design patterns, and any approaches out there.

So let's get back to the main topic. We usually use cache like this:

// for example, Country is an entity that we don't change much
// CountryController.php
public function index(): JsonResponse
{
    $countries = Cache::rememberForever(
        'countries-list',
        fn () => Country::query()->pluck('name', 'code'),
    );

    return new JsonResponse(compact('countries'));
}

public function store(CountryStoreRequest $request): JsonResponse
{
    $country = Country::create($request->validated());

    Cache::forget('countries-list');

    return new JsonResponse();
}

public function update(CountryUpdateRequest $request, Country $country): JsonResponse
{
    $country->update($request->validated());

    Cache::forget('countries-list');

    return new JsonResponse();
}

public function destroy(CountryDestroyRequest $request, Country $country)): JsonResponse
{
    $country->delete();

    Cache::forget('countries-list');

    return new JsonResponse();
}

The problems:

  • Hardcoded cache key across the codebase, which can easily leak to human error when developing features.

  • The literal 'countries-list' could be anything in the codebase, imagine a big codebase and you want to search for the cache stuff only, it's a nightmare.

And that's just for a fixed cache key, imagine you want to cache for specific users, or businesses, etc. The complex just becomes bigger.

The solution

From the statement above:

Laravel allows us to use any design patterns, and any approaches out there.

Yes, don't limit our implementation, let's add more spicy stuff into the codebase. Let's go above and beyond ✈️

I'll create an AbstractDataCache with some simple implementations:

<?php

namespace App\DataCache;

use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Cache;

abstract class AbstractDataCache
{
    /**
     * Concrete class must implement and return the cache key
     */
    abstract public function getCacheKey(): string;

    /**
     * Concrete class must know how to compute and return the data that need to be cached
     */
    abstract public function computeCache(): mixed;

    /**
     * By default, data will be cached for an hour
     *
     * @return CarbonImmutable
     */
    public function getCacheExpiration(): CarbonImmutable
    {
        return CarbonImmutable::now()->addHour();
    }

    public function get(): mixed
    {
        return Cache::remember(
            $this->getCacheKey(),
            $this->getCacheExpiration(),
            fn () => $this->computeCache()
        );
    }

    /**
     * Clear the cache data
     */
    public function clear(): void
    {
        Cache::forget($this->getCacheKey());
    }

    /**
     * Clear and re-cache the data
     */
    public function rebuild(): void
    {
        $this->clear();
        $this->get();
    }
}

Then I'll create CountriesListDataCache and extends and implements the required methods:

class CountriesListDataCache extends AbstractDataCache
{
    protected function getCacheKey(): string
    {
        return 'countries-list';
    }

    protected function computeCache(): array
    {
        return Country::query()->pluck('name', 'id');
    }

    protected function getCacheExpiration(): CarbonImmutable
    {
        return CarbonImmutable::now()->addYear();
    }
}

Taking into action:

public function index(CountriesListDataCache $dataCache): JsonResponse
{
    $countries = $dataCache->get();

    return new JsonResponse(compact('countries'));
}

public function store(
    CountryStoreRequest $request,
    CountriesListDataCache $dataCache
): JsonResponse {
    $country = Country::create($request->validated());

    $dataCache->rebuild(); // or clear

    return new JsonResponse();
}

Achievements

So with that, what do we achieve?

  • A new layer to manage the caches (key, time, values)

    • No more hardcoded string or string concat/manipulation

    • No more duct tape caching stuff

  • Maintaining the cache across the app is way better.

    • We also have a dedicated folder for cache stuff.
  • Can use the dependency injection just fine.

  • Testable and super easy-to-write test cases.

Q/A

  • Q: What if I need some additional entities to compute the cache data?

    • A: pass them to the constructor and then you can use them normally fine (same approaches as Queue)
  • Q: How to use dependency injection?

    • A: use the helper app function to get your desired instance for your computation.

      • Additionally, I'd love to make the computeCache as same as the handle of Queue class, will do soon

Conclusion

Well, that's that for the DataCache topic. I hope you guys enjoy it. I have been using that approach for some of my projects, so far so good.

If you have any other ideas or questions, comment below!

Cheers, and see you next time!