As a developer working on large Laravel applications, you’ve probably encountered the performance hit that comes with frequent database queries. Eager loading and caching can only do so much to mitigate this issue. At some point, you may have found yourself manually implementing query caching for specific models or even worse, resorting to using third-party libraries with limited flexibility.
You’ll build a robust automated caching system for your Laravel application by the end of this tutorial. Specifically, you’ll learn how to cache Eloquent queries with automatic invalidation, create a custom cache key resolver, and integrate GitHub Actions to automatically test your caching layer and rebuild Laravel’s framework caches. With these tools in hand, you’ll be able to optimize your application’s performance without sacrificing maintainability or ease of use.
Setting Up GitHub Actions for Laravel
To automate your caching pipeline with GitHub Actions, you’ll first need to set up a basic workflow for your Laravel project. This will involve creating a new file in the .github/workflows directory of your repository.
Create a new file called laravel.yml (or any other name that makes sense for your project) within this directory:
name: Laravel Workflow
on:
push:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install dependencies
run: |
composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Create .env file
run: |
cp .env.example .env
php artisan key:generate
- name: Run tests
run: php artisan test
This workflow will trigger on push events to the main branch, install dependencies, create an .env file from .env.example, and run your test suite.
In your project’s root directory, create a new file called .github/workflows/caching.yml. This file will contain the caching-specific steps of your pipeline:
name: Laravel Query Caching
on:
push:
branches: [main]
jobs:
cache-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Cache Composer dependencies
uses: actions/cache@v6
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
- name: Install dependencies
run: |
composer install --no-interaction --prefer-dist --optimize-autoloader
Note that we’re setting up the basic workflow for our project. In the next section, we’ll configure Eloquent query caching in Laravel.
Configuring Eloquent Query Caching in Laravel
Eloquent query caching is built on top of Laravel’s cache, so we first need to configure a cache store through Laravel’s cache configuration file. Open your project’s config/cache.php file and locate the 'stores' => [] array.
// config/cache.php
return [
// ... other configurations ...
'default' => env('CACHE_STORE', 'file'),
'stores' => [
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
],
];
In the stores configuration, we’re setting up a file-based cache store. You can adjust this to use other cache drivers like Redis or Memcached if your project requires it.
Next, we need to enable query caching for Eloquent. Eloquent doesn’t cache query results on its own, so we’ll add a cached lookup to the model with Cache::remember() and clear the cached entry whenever the model changes. Open your app/Models/User.php file:
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Cache;
class User extends Authenticatable
{
// ... existing traits and properties ...
public static function cacheKeyForEmail(string $email): string
{
return 'eloquent:users:email:' . md5($email);
}
public static function findByEmailCached(string $email): ?self
{
return Cache::remember(
static::cacheKeyForEmail($email),
now()->addMinutes(120),
fn () => static::where('email', $email)->first()
);
}
protected static function booted(): void
{
static::saved(function (User $user) {
// Forget both the old and the new email in case the email changed
Cache::forget(static::cacheKeyForEmail($user->getOriginal('email') ?? $user->email));
Cache::forget(static::cacheKeyForEmail($user->email));
});
static::deleted(fn (User $user) => Cache::forget(static::cacheKeyForEmail($user->email)));
}
}
Cache::remember() returns the cached result when one exists; otherwise it runs the query and stores the result for 120 minutes. The saved and deleted model events remove the cached entry whenever the user changes, so frequently accessed queries are served from the cache without ever returning stale data, reducing the load on your database.
With these changes, Eloquent is now configured for query caching. However, we still need to automate testing the cache and rebuilding Laravel’s caches using GitHub Actions. In the next section, we’ll create a cache key resolver using Laravel’s service container to automatically generate unique cache keys for our queries.
Creating a Cache Key Resolver using Laravel’s Service Container
To cache query results for entire requests, we need a way to determine cache keys for our database queries. We can leverage Laravel’s service container to create a cache key resolver that generates unique cache keys based on the query parameters.
First, let’s create a new class CacheKeyResolver.php in the app/Services directory:
// app/Services/CacheKeyResolver.php
namespace App\Services;
use Illuminate\Http\Request;
class CacheKeyResolver
{
public function resolve(Request $request): string
{
// Generate a unique cache key based on the query parameters
$query = $request->query();
ksort($query);
$cacheKey = md5(json_encode([
$request->method(),
$request->url(),
$query,
]));
return 'eloquent:' . $cacheKey;
}
}
This class uses Laravel’s Illuminate\Http\Request object to extract the query parameters and generates a unique, deterministic cache key using the md5 function.
Next, let’s register the CacheKeyResolver service in the Laravel container:
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use App\Services\CacheKeyResolver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Register the CacheKeyResolver service
$this->app->singleton(CacheKeyResolver::class);
}
// ...
}
With the resolver bound in the container, you can type-hint it in any controller method that caches query results:
// app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use App\Models\User;
use App\Services\CacheKeyResolver;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
public function index(Request $request, CacheKeyResolver $resolver)
{
$users = Cache::remember($resolver->resolve($request), now()->addMinutes(120), function () {
return User::latest()->take(50)->get();
});
return response()->json($users);
}
}
Automating Eloquent Query Caching with GitHub Actions
Setting up a GitHub Action for Eloquent Query Caching
In the previous sections, we’ve set up Eloquent query caching and created a cache key resolver using Laravel’s service container. Now, let’s automate testing and rebuilding our caches by creating a GitHub Action that runs whenever our code changes.
First, create a new file in your repository’s .github/workflows directory: cache-queries.yml. This is where we’ll define the workflow for automating Eloquent query caching.
name: Cache Queries
on:
push:
branches:
- main
paths:
- app/Models/**
- database/seeders/**
- database/migrations/**
- routes/**
- app/Providers/**
jobs:
cache-queries:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install dependencies
run: |
composer install --no-interaction --prefer-dist
- name: Prepare environment
run: |
cp .env.example .env
php artisan key:generate
- name: Run caching tests
run: |
php artisan test
- name: Rebuild framework caches
run: |
php artisan optimize
This workflow listens for changes on specific files and directories, installs dependencies, runs the test suite that verifies our query caching, and then runs the optimize Artisan command. This rebuilds Laravel’s configuration, event, route, and view caches, so any change that breaks caching fails the build before it reaches production.
Note that we’re running this job on every push to the main branch, but you can adjust the on trigger according to your project’s needs.
With this workflow in place, you’ll have a robust system for automatically verifying your query caching and rebuilding Laravel’s caches whenever code changes are pushed to your repository.
Testing the Automated Caching Setup
Now that we have our automated caching setup in place, it’s essential to verify its correctness and performance. Let’s create a test suite to validate our implementation.
Firstly, let’s update our tests/Unit/Models/UserTest.php file with some test cases:
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_caching_query()
{
User::factory()->create(['name' => 'John Doe', 'email' => 'john.doe@example.com']);
$user = User::findByEmailCached('john.doe@example.com');
// Assert that the query was cached
$this->assertNotNull($user);
$this->assertTrue(cache()->has(User::cacheKeyForEmail('john.doe@example.com')));
// Update the user to trigger cache update
$user->update(['name' => 'John Doe Updated']);
// Assert that the updated query is not retrieved from cache
$this->assertFalse(cache()->has(User::cacheKeyForEmail('john.doe@example.com')));
$updatedUser = User::findByEmailCached('john.doe@example.com');
$this->assertEquals('John Doe Updated', $updatedUser->name);
}
}
We’ll also need to add a test for the cache key resolver. Since we’re using Laravel’s service container, we can create a test case for it in tests/Unit/Services/CacheKeyResolverTest.php:
use Tests\TestCase;
use App\Services\CacheKeyResolver;
use Illuminate\Http\Request;
class CacheKeyResolverTest extends TestCase
{
public function test_resolve_cache_key()
{
$resolver = app(CacheKeyResolver::class);
// Assert that the cache key is correctly resolved
$cacheKey = $resolver->resolve(Request::create('/users', 'GET', ['page' => 2]));
$this->assertStringStartsWith('eloquent:', $cacheKey);
$this->assertEquals($cacheKey, $resolver->resolve(Request::create('/users', 'GET', ['page' => 2])));
$this->assertNotEquals($cacheKey, $resolver->resolve(Request::create('/users', 'GET', ['page' => 3])));
}
}
These test cases will ensure our automated caching setup is working as expected.
Integrating Dependabot to Keep Dependencies Up-to-Date
Now that our automated caching setup is in place, let’s ensure our dependencies remain up-to-date. This is crucial for maintaining security and stability in our project.
Dependabot is built into GitHub, so there’s no package to install with Composer. Instead, Dependabot version updates are enabled by committing a configuration file to your repository.
Create the dependabot.yml file in the .github directory by running the following command:
mkdir -p .github && touch .github/dependabot.yml
We need to fill this file with the package ecosystems we want Dependabot to monitor and how often it should check them.
Here’s an example of what the dependabot.yml file might look like:
version: 2
updates:
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
This configuration tells Dependabot to check our Composer dependencies (such as laravel/framework) and the actions used in our workflows every week, and to open a pull request whenever an update is available.
Finally, we need to configure GitHub Actions to check our dependencies for known vulnerabilities on each push event. Add the following YAML snippet to your .github/workflows/composer-audit.yml file:
name: Composer Audit
on:
push:
branches: [ main ]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Audit dependencies
run: composer audit
Dependabot keeps our dependencies updated automatically through pull requests, while this workflow fails the build whenever changes pushed to the main branch include a package with a known security advisory. With this setup, we can focus on developing and testing our application, knowing that our dependencies are always up-to-date.
By following these steps, we’ve integrated Dependabot into our workflow, ensuring our project’s dependencies remain secure and stable.
Monitoring and Debugging the Automated Caching Process
Now that our query caching and GitHub Actions workflows are in place, it’s essential to monitor and debug the process to ensure everything works as expected.
To monitor the automated caching process, you can use Laravel’s built-in logging features. In your config/logging.php file, update the channels array to include a new channel for cache events:
'cache' => [
'driver' => 'daily',
'path' => storage_path('logs/cache.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 7,
],
Then, in your app/Providers/AppServiceProvider.php file, add the following code to log every cache hit and miss to that channel:
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
// ...
public function boot()
{
// ...
Event::listen(CacheHit::class, function (CacheHit $event) {
Log::channel('cache')->debug('Cache hit', ['key' => $event->key]);
});
Event::listen(CacheMissed::class, function (CacheMissed $event) {
Log::channel('cache')->debug('Cache missed', ['key' => $event->key]);
});
}
To debug issues with automated caching, you can check the GitHub Actions logs for errors or exceptions. You can also use Laravel’s built-in artisan command to clear and re-cache your application:
php artisan optimize:clear && php artisan optimize
This clears Laravel’s cached configuration, routes, views and events (plus the application cache) and rebuilds the framework caches, allowing you to troubleshoot any caching-related issues.
With these tools in place, you’ll be well-equipped to monitor and debug the automated caching process.
Frequently Asked Questions
How do I automate Eloquent query caching using GitHub Actions?
To automate Eloquent query caching, you’ll need to create a new workflow file in the .github/workflows directory of your repository and add steps that run your caching tests and rebuild Laravel’s caches with php artisan optimize.
What is the purpose of creating two separate workflow files (laravel.yml and caching.yml)?
The first workflow file sets up the basic build process, while the second workflow file contains the caching-specific steps, such as caching Composer dependencies between runs.
How do I configure Eloquent query caching in Laravel’s cache configuration file?
You’ll need to open your project’s config/cache.php file and locate the 'stores' => [] array, where you can adjust the cache driver and settings as needed. Eloquent results are then cached in your models or controllers with Cache::remember().
Can I use a different cache driver like Redis or Memcached instead of the default file-based cache store?
Yes, you can adjust the cache configuration to use other drivers like Redis or Memcached if your project requires it.
