Laravel and Redis 5 - A Functional Example

Last updated: October 8th 2020

Introduction

In this article we go through some functional examples in order to learn how to use Redis 5 with laravel in real-life like situations. 

Requirements

Before you start, please make sure you have read the introductory article Laravel and Redis 5 - Introduction, Installation and basic use

Functional Example with Blog Posts in Laravel

In this tutorial we are using Laravel framework version 8+. There might be slight differences with your Laravel installation, especially if it is older. For instance: artisan will put our models in the App\Models directory while in older versions they might end in the App directory only!

In this example, we will continue to explore possibilities of using Redis with the Laravel framework. We will create a simple blog post model without Auth user interaction to keep this tutorial simple as possible. Let's create a migration file for a posts table:

php artisan make:migration create_posts_table

Now, inside the newly created migration file replace all code with this:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
			$table->string('title', 100);
			$table->string('category', 50);
			$table->string('tags', 250);
			$table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

Next up is making model for posts:

php artisan make:model Post

...and the code for our Post model:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
	
	protected $primaryKey = 'id';
	protected $table = 'posts';
	protected $fillable = array('title', 'category', 'tags', 'content');
	
	
}

Now, let's seed the database with fake data:

php artisan make:seeder PostSeeder

And change these two files

// database/seeders/DatabaseSeeder.php
namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
	        PostSeeder::class,
	    ]);
    }
}

..and:

// database/seeders/PostSeeder.php
namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Faker\Factory as Faker;

class PostSeeder extends Seeder
{

	public function run()
	{
		$faker = Faker::create();
		
		$limit = 10;
		for ($i = 0; $i < $limit; $i++) {
			DB::table('posts')->insert([
				'title' => substr($faker->sentence($faker->numberBetween(1,5),true), 0, -1), //Remove . from the end
				'category' => $faker->randomElement(['travel', 'life style', 'in focus', 'globe']),
				'tags' => implode(',', $faker->randomElements(['Meklowski', 'Incident', 'East', 'Charles Jo.', 'Space', 'Universe', 'DIY', 'Tuts'], $faker->numberBetween(1,8)) ),
				'content' => $faker->text(6000)
			]);
		}
	}
}

Then run:

php artisan db:seed

Ok, so what we have done here is we have made model for posts and filled the database with fake data. Explaining the Faker factory is another topic, although you can check the official docs here.

Every iteration through the seed command will fill our database with another ten fake posts, so you are free to add more or change $limit number.

OK, so let's build our controller, view and route for displaying the posts:

php artisan make:controller PostController
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Post;

class PostController extends Controller
{
    public function displayPost( $id ){
    	$post = Post::findOrFail($id);
		return view('post', compact('post'));
    }
}

Then create a blade file /resources/views/post.blade.php :

<!DOCTYPE html>
<html>
<head>
 <title>Webdock Laravel - Redis</title>
 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha256-aAr2Zpq8MZ+YA/D6JtRD3xtrwpEz2IqOS+pWD/7XKIw=" crossorigin="anonymous" />
 <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha256-OFRAJNoaD8L3Br5lglV7VyLRf0itmoBzWUoM+Sji4/8=" crossorigin="anonymous"></script>
</head>
<body>
 <div class="row">
 <div class="col-md-8 offset-2">
 <div class="card mt-5">
 <h3 class="text-center p-3"><strong>{{ $post->title }}</strong></h3>
 </div>
 <div class="card-body">
 <div class="py-2">Category: {{ $post->category }}</div>
 <div class="py-2">Tags: {{ $post->tags }}</div>
 <p class="p-3">{{ $post->content }}</p>
 </div>
 </div>
 </div>
</body>
</html>

Great, now we can test our route by visiting /post/__SOME_POST_ID__

Redis and posts

In this section we are going to put our posts into the Redis database. Here we have two approaches:

Store all posts in Redis or just the ones requested. We are going with the second scenario: once the post is requested by a visitor, we are going to pull it from MySQL and store it in Redis before serving it to the front-end.

This way what we keep in Redis are current and most requested posts. Essentially using Redis as a fast cache layer for our data. Imagine sharing a post on social media which you know will generate a lot of traffic. Since we are expecting high traffic on the targeted post, we can make sure the post will be pulled from MySQL only once, and all subsequent calls will use Redis to serve it quickly until our Redis key TTL expires, and thus free up memory for other "currently requested" posts.

This is very useful for dynamic web sites and/or services, where a lot of content created may become viral, and forgotten/archived later on. So, up-to-date content is always available through fast memory based storage, while not being bloated with less relevant content.

First of all, as suggested in the Laravel documentation, we are going to use full namespacing for the Redis class instead of aliases. Add this code to your Post.php model:

use Illuminate\Support\Facades\Redis; 

Now, add this function to the same Post model:

public static function redisFindOrFail( $id ){
	if ( $post = Redis::get('post:'.$id) ) {
		Redis::expire('post:'.$id, 60*60*2);
		$post = json_decode($post, true);
		return  new Post($post);
	}
	else {
		$post = self::findOrFail($id);
		Redis::setex('post:'.$id, 60*60*2, json_encode($post));
		return $post;
	}
}

Also, edit the function in the PostController.php

public function displayPost( $id ){
	$post = Post::redisFindOrFail($id);
	return view('post', compact('post'));
}

What we have done here is quite simple: we are looking for the required post in Redis first, then if it is not present pull data from MySQL, place it in Redis (for 2hrs) and then display it. Also, if data is present in Redis, we reset the TTL (Time To Live) expiry timer for another 2 hrs.

Feel free to experiment with different timings and browse through the posts. You can watch for changes in Redis Commander.

Category search results

In this section we will store in Redis our search results by category. We need a new View file, and some code added to Post.php, PostController.php and new route:

Create a new file category.blade.php

<!DOCTYPE html>
<html>
<head>
 <title>Webdock Laravel - Redis</title>
 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha256-aAr2Zpq8MZ+YA/D6JtRD3xtrwpEz2IqOS+pWD/7XKIw=" crossorigin="anonymous" />
 <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha256-OFRAJNoaD8L3Br5lglV7VyLRf0itmoBzWUoM+Sji4/8=" crossorigin="anonymous"></script>
</head>
<body>
 <div class="row">
 <div class="col-md-8 offset-2">
 <div class="card mt-5">
 <h3 class="p-3">Category: <strong>{{ $category }}</strong></h3>
 </div>
 <div class="card-body">
 @foreach ( $posts as $post )
 <div class="row py-2">
 <div>
 <h3>{{ $post->title }}</h3>
 <p>{{ substr($post->content, 0, 250) }}...</p>
 </div>
 </div>
 @endforeach
 </div>
 </div>
 </div>
</body>
</html>

Add a new route to web.php

Route::get('category/{name}', 'PostController@displayCategory'); 

Append to PostController.php a new function

public function displayCategory( $name ){
	$posts = Post::redisCategory( $name );
	return view('category', ['posts' => $posts, 'category'=> $name ]);
}
  

...and our Post.php model:

public static function redisCategory( $name ){
	if ( $redis_arr = Redis::get('category:'.$name) ) {
		Redis::expire('category:'.$name, 60*60*2);
		$redis_arr = json_decode($redis_arr);
		$posts = [];
		foreach ( $redis_arr as $id ){
			$posts[] = self::redisFindOrFail($id);
		}
	}
	else{
		$posts = Post::where('category', '=', $name)->get();
		if ( !empty($posts) ){
			$redis_arr = [];
			foreach ( $posts as $post ){
				$redis_arr[] = $post->id;
				Redis::setex('post:'.$post->id, 60*60*2, json_encode($post));
			}
			Redis::setex('category:'.$name, 60*60*2, json_encode($redis_arr));
		}
	}
	return $posts;
}

Now visit your new route /category/__SOME_CATEGORY__ and populate it with categories found in our database seeder file. You can check Redis Commander too, and see how our data trees are formed in real time.

Since all the magic happens in our Post model, let's explain it a bit. There are two very important things we need to comment on: First, as a result of the database query, we stored only the IDs of post objects. Second, we used the already coded function from our model to retrieve full POST objects from Redis. Worth mentioning, if CATEGORY result is already there, we might not trigger MySQL at all.

With this approach we achieved several things at once: we avoid duplicating data in our Redis database which is great in terms of economizing our server resources, we used short expressions for results, which lead us to easily cross-section arrays e.g. multi category posts, etc.

Extracting posts from MySQL by category is not a such demanding task, but other complex queries might be, especially during heavy web traffic, so in addition to other benefits, "caching" search results is always a good idea.

In the same fashion we can build a route to display tags, with minor changes in the code, only pay attention for SQL query: '=' becomes 'LIKE' and $name becomes %$name%.

Eloquent events and Redis

In this section we show you how to add an event for post creation, which will add our new post object to Redis for further use.

First edit this file /app/Providers/EventServiceProvider.php to register event

protected $listen = [
	\App\Events\PostSaved::class => [
            \App\Listeners\PostSaved::class,
	],
    ];

Then run Artisan command:

$ php artisan event:generate

Edit /app/Events/PostSaved.php to look like this:

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

use App\Models\Post;

class PostSaved
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
	
	public $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

and /app/Listeners/PostSaved.php to look like this:

namespace App\Listeners;

use App\Events\PostSaved as PostSavedEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

use App\Models\Post;
use Illuminate\Support\Facades\Redis; 
 
class PostSaved
{

    public function __construct()
    {
        //
    }

    public function handle(PostSavedEvent $event)
    {
    	Redis::del('post:'.$event->post->id);
        return Post::redisFindOrFail($event->post->id);
    }
}

Add to your Post model App/Models/Post.php

protected $dispatchesEvents = [
	'saved' => \App\Events\PostSaved::class,
];

What we have done here is quite simple to explain: whenever a post is saved, our Redis database is updated with fresh data. This includes post creation, and edit.

The significance of the events and Redis interaction is huge. Implementing events logic, we can automate a lot of Redis operations. Although not displayed in the code here, events such as delete and/or recreate search results partially or fully is all up to you and the logic of your application.

Here is the list of available events:

  • retrieved
  • creating
  • created
  • updating
  • updated
  • saving
  • saved
  • deleting
  • deleted
  • restoring
  • restored

From this list it is pretty straightforward what these events do with slight clarifications:

The difference between saving and saved event has to do with the time of occurrence; saving is triggered before the actual database interaction, while saved is triggered after. In our case it is useful to update Redis category (or tags) tree during post update: before update we should delete all post occurrences in categories, while afterwards we will recreate it especially if there is change in post category markings.

In this section we explained the benefits of using events in the context of the Redis database. There are a plenty of other use-cases and implementations, but our main goal is to point out the significance and benefit of using events.

Conclusion

In this tutorial series we explained the basic use of Redis with the Laravel framework. There are plenty of ways you can implement Redis in your application and it is heavily dependent on the nature of the project itself, So, use this guide as a resource for potential ideas, rather than a "best practice" type guide.

What is next? In this article we just scratched the possibilities of Redis. We used only one data type to store our objects. Redis offer many more: hashes, streams, etc. Storing user sessions (like shopping carts), caching content, implementing counters are just a few of the many possible ways you can boost your app with Redis.

Concerning the Laravel framework in general, your next stop should be the built-in cache engine. According to Laravel docs: Cache driver is an expressive, unified API for various caching backends. Many things we already mentioned comes out of the box, so we encourage you to use it.

RAM driven databases are already a standard in the industry, so we hope this tutorial provided you with some basic guidelines.

Series Author: Aleksandar Milivojevic is a PHP programmer for over 10 years (Laravel and Wordpress). Writing articles in brief pauses between projects, to help out the community and popularize PHP in general.

Related articles