Using Redis With Laravel Framework


03 May
03May

Redis is an in-memory data structure project implementing a distributed, in-memory key-value database, and using it with a framework like Laravel to store data that will be mostly be read and won’t change multiple times can significantly increase speed of fecthing data compared to a structured database like Mysql.

In this tutorial, you will be learning

  • How to install Redis and the installation options available to you
  • Redis Data types with relatable examples
  • Create a simple ecommerce using Laravel with Redis for data storage
  • Using Laravel Websocket with Redis queue to show live update to users without having to refresh the page

Requirements

For the purpose of this tutorial, you must have

  • A new Laravel 7.x project installed in your web root directory
  • Must be using a Linux machine or Mac OS
  • Must have composer installed globally on your machine
  • Must be using PHP version higher than 7.2

Before anything, you need to install redis client on your machine if don’t already have it. The most suggested way to do so is to directly compile it from source, to do so run the following commnads in this order on your terminal.

wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make

Finally do this

cp src/redis-cli /usr/local/bin/
chmod 755 /usr/local/bin/redis-cli

#Optionally
sudo cp src/redis-server /usr/local/bin/
chmod 755 /usr/local/bin/redis-cli

See if everything works fine by running this command from your terminal

redis-cli  

If eveything works fine, you should be taken into the redis command line interface.

Installing PHP Redis

There are two option available for installing Redis with laravel, the easiest and the first method is using composer to install predis/predis package via Composer.

composer require predis/predis

As at Laravel 7.x this package has been abandon by its original author and Laravel is considering to remove it in future release.

Option two which is the most advised option but more tideous one is to install the PHPRedis extension which provides an API to communicate with the Redis server. Unlike the predis composer package, its more reliable and fast and can be used in bigger applications.

We will install the PHPRedis extension using PECL. For those who do not know, PECL is a repository for PHP Extensions.

Installation on Ubuntu

sudo apt-get -y install gcc make autoconf libc-dev pkg-config  
sudo peclX.Y-sp install redis

Note: Replace peclX.Y with what ever your php version is, you can check by running php -v from your terminal, so if your php version is 7.2 your command will be sudo pecl7.2-sp install redis

Once the extension is installed successfully, create a configuration file for the redis extension to be loaded then restart PHP by running the following command.

sudo bash -c "echo extension=redis.so > /etc/php5.X-sp/conf.d/redis.ini"  
sudo service php7.X-fpm-sp restart

Installation on MacOs

Run

pecl install redis

Now edit your php.ini, the loaded version. To find out which one is the loaded .ini file, create a phpinfo.php file and insert <?php echo phpinfo(); . Check the output for the loaded version, then edit it and remove the line below or comment it out.

extension="redis.so"

Save and exit the the php.ini file.

Finally create /usr/local/etc/php/7.X/conf.d/ext-redis.ini edit it using vim or nano, where you will substitute the 7.X with your own php version.

vi /usr/local/etc/php/7.X/conf.d/ext-redis.ini

Then paste the below content inside this new file

[redis]
extension="/usr/local/Cellar/php/7.X.Y/pecl/20180731/redis.so"

Restart php-fpm or if you are using Laravel valet run valet restart

Redis Data Types

Redis has different datatypes namely, String, Lists, Sets, Sorted Sets, Hashes & Bitmaps and Hyperlogs. We will be discussing all the available Data types except for the last one.

For the purpose of this tutorial we will be trying out all the data types example from the redis command line interface. So open a new terminal and run the command

redis-cli 

String

This is the most basic datatype available. Redis Strings are binary safe, this means that a Redis string can contain any kind of data, for instance a JPEG image or a serialized object.

Let us try out an example from the redis command line

127.0.0.1:6379> SET user:1:name Oluwafemi  
  
127.0.0.1:6379> GET user:1:name  
  
"Oluwafemi"

Lists

Is a list of strings, sorted by insertion order, where Items can either appended to the start of a list or to the end. Larvel makes use of this datatype to store jobs in queue which are then executed the order they were added to the list.

127.0.0.1:6379> LPUSH blog:1:tags laravel php redis  
  
127.0.0.1:6379> LRANGE blog:1:tags 0 -1  
1) "redis"  
2) "php"  
3) "laravel"  
  
127.0.0.1:6379> RPUSH blog:1:tags pecl  
(integer) 4  
  
127.0.0.1:6379> LRANGE blog:1:tags 0 -1  
1) "redis"  
2) "php"  
3) "laravel"  
4) "pecl"

Sets

Sets are an unordered collection of Strings. It is possible to add, remove, and test for existence of members in a set. They do not allow repeated items and when retrieving items they are not returned in the same order they are are entered unlike lists.

And like a set, you can perform actions between two or more sets such as finding the intersection or union .

127.0.0.1:6379> SADD user:1:interests horror comedy violent drama inspiring  
(integer) 5  
  
127.0.0.1:6379> SMEMBERS user:1:interests  
1) "drama"  
2) "comedy"  
3) "violent"  
4) "horror"  
5) "inspiring"  
  
127.0.0.1:6379> SISMEMBER user:1:interests drama  
(integer) 1  
  
127.0.0.1:6379> SISMEMBER user:1:interests dark  
(integer) 0  
  
127.0.0.1:6379> SADD user:2:interests horror drama inspiring  
(integer) 3  
  
127.0.0.1:6379> SINTER user:1:interests user:2:interests  
1) "drama"  
2) "inspiring"  
3) "horror"  
  
127.0.0.1:6379> SUNION user:1:interests user:2:interests  
1) "violent"  
2) "comedy"  
3) "drama"  
4) "inspiring"  
5) "horror"  
  
127.0.0.1:6379> SCARD user:2:interests  
(integer) 3  
  
127.0.0.1:6379> SSCAN user:2:interests 0 match horror  
1) "0"  
2) 1) "horror"

Hashes

Redis Hashes are maps between string fields and string values, so they are the perfect data type to represent objects. In a chat between two users, each message sent out and it deatils can be represented as a hashmap.

127.0.0.1:6379> HMSET chats:1 message Hello user_id 1  
OK  
  
127.0.0.1:6379> HMSET chats:2 message "You there!" user_id 1  
OK  
  
127.0.0.1:6379> HMSET chats:2 message "Yes I am" user_id 2  
OK  
  
127.0.0.1:6379> HGET chats:1 message  
"Hello"  
  
127.0.0.1:6379> HGET chats:2 user_id  
"2"  
  
127.0.0.1:6379> HGETALL chats:2  
1) "message"  
2) "Yes I am"  
3) "user_id"  
4) "2"  
  
127.0.0.1:6379> HSET chats:2 message "Edited message"  
(integer) 0  
  
127.0.0.1:6379> HGETALL chats:2  
1) "message"  
2) "Edited message"  
3) "user_id"  
4) "2"  

Sorted Sets

Sorted sets are similar to sets except that every member of a Sorted Set is associated with a score, that is used in order to take the sorted set ordered, from the smallest to the greatest score.

Imagine a sorted sets of a user messages for a chat history between two users. This way we can be sure when retriving the messages to display that they are returned in the order of timestamps they were stored where the timestamp serve as the score.

127.0.0.1:6379> ZADD chat_between:user_1:user_2 1588542249 1 1588542252 2 1588542273 3    
(integer) 3  
  
127.0.0.1:6379> ZRANGE chat_between:user_1:user_2 0 -1  
1) "1"  
2) "2"  
3) "3"  
  
127.0.0.1:6379> ZRANGE chat_between:user_1:user_2 0 -1 withscores  
1) "1"  
2) "1588542249"  
3) "2"  
4) "1588542252"  
5) "3"  
6) "1588542273"

Almost all action that can be performed on a set can be done for a sorted set.

To learn more about Redis dataset, visit the offical documentation

Ecommerce App using Laravel & Redis

At this point I expect you have installed a new laravel project in your web root. Next setup the authentication scaffold and visit your app in the browser.

If you have not done so, run the following commands

composer create-project --prefer-dist laravel/laravel ecommerce  
  
cd ecommerce  
  
composer require laravel/ui  
  
php artisan ui bootstrap --auth  
  
npm install  
  
npm run dev

Create a new database mysql ecommerce

mysql -u root -p  
  
  
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.  
  
  
mysql> create database ecommerce ;  
Query OK, 1 row affected (0.24 sec)

Open the ecommerce Laravel project in your PHP editor, I use PHPStorm, then edit your project .env configuration to match the database details.

DB_CONNECTION=mysql  
DB_HOST=127.0.0.1  
DB_PORT=3306  
DB_DATABASE=ecommerce  
DB_USERNAME=user  
DB_PASSWORD=yourpassword

Then run php artisan migrate to create the users and password reset tables in the ecommerce database.

Database Schema

To give you a better understanding of how Redis will play a role in storing our data, below is a sketch of the database schema. I know you are thinking isn’t this suppose to be a Nosql databse, where does a schema comes in here.

Relax a “schema” simply refers to the organization of data as a blueprint of how the database is constructed.

From here it gets easier, now that we know better how our data should be represented in Redis.

Adding a Product

Add the following lines to your route file web.php

Route::get('/products/create', 'ProductController@create')->name('product.new');  
Route::post('/products/create', 'ProductController@store')->name('product.store');  
Route::get('/products/all', 'ProductController@viewProducts')->name('product.all');

Create a new file resources/view/product/create.blade.php that will carry our new produc form, paste the content below.

@extends('layouts.app')  
  
@section('content')  
<div class="container">  
 <div class="row justify-content-center">  
 <div class="col-md-8">  
 <div class="card">  
 <div class="card-header">New Product</div>  
 <div class="card-body">  
 <form method="POST" action="{{route('product.store')}}">  
 {{csrf_field()}}  
  <div class="form-group">  
 <label for="product_name">Product Name</label>  
 <input type="text" class="form-control" name="product_name" id="product_name" required placeholder="ex Headset Jack">  
 </div>  
 <div class="form-group">  
 <label for="product_image">Product Image</label>  
 <input type="text" class="form-control" name="product_image" id="product_image" required placeholder="Image url">  
 </div>  
 <div class="form-group">  
 <label for="tags">Tags</label>  
 <input type="text" class="form-control" name="tags" id="tags" required placeholder="Separate tags by comma">  
 </div>  
 <button type="submit" class="btn btn-primary">New Product</button>  
 </form>  
 </div>  
 </div>  
 </div>  
 </div>  
 </div>  
@endsection

Next create a new file resources/view/product/browse.blade.php to display all the products added.

@extends('layouts.app')  
  
@section('content')  
  <div class="container">  
 <div class="row justify-content-center">  
 <div class="col-md-12">  
  @if($products)  
  <div class="row">  
 <div class="col-9">  
 <div class="col-12 mb-1">  
 <a href="{{route('product.new')}}">Add New Product</a>  
 </div>  
  @foreach($products as $product)  
  <div class="col-4 float-left mb-1">  
 <div class="card">  
 <img class="card-img-top" height="260" src="{{$product['image']}}" alt="Card image cap">  
 <div class="card-body text-center">  
 <h5 class="card-title">{{$product['name']}}</h5>  
 </div>  
 </div>  
 </div>  
  @endforeach  
  </div>  
 <div class="col-3 border">  
  @foreach($tags as $tag)  
  <a class="btn btn-sm btn-primary px-2 py-1 m-1" href="?tag={{$tag}}" role="button">{{$tag}}</a>  
  @endforeach  
  </div>  
 </div>  
  @else  
  <div class="card">  
 <div class="card-header">Browse Products</div>  
 <div class="col-8">  
 <div class="alert alert-success mt-2" role="alert">  
  Empty products! <a href="{{route('product.new')}}">Add Product</a>  
 </div>  
 </div>  
 </div>  
  @endif  
  </div>  
 </div>  
 </div>  
@endsection

Finally update your ProductController to match the below code, don’t be yet over whelmed, with all the method in this controller, I will explain every bit to you shortly.

<?php  
  
namespace App\Http\Controllers;  
  
use Illuminate\Http\Request;  
use Illuminate\Support\Facades\Redis;  
  
class ProductController extends Controller  
{  
      public function __construct()  
      {  
        $this->middleware('auth');  
      }  
      
      public function create()  
      {  
         return view('products.create');  
      }
      
      public function store(Request $request)  
      {  
         $tags = explode(',',$request->get('tags'));  
          $productId = self::getProductId();  
      
          if(self::newProduct($productId, [
          'name' => $request->get('product_name'),
          'image' => $request->get('product_image'),
          'product_id' => $productId
          ])){  
          self::addToTags($tags);  
          self::addToProductTags($productId, $tags);  
          self::addProductToTags($productId, $tags);  
         }  
         
         return redirect()->route('product.all');  
     }  
     
      public function viewProducts()  
      {  
         $tags = Redis::sMembers('tags');  
         $products = self::getProducts();  
      
          return view('products.browse')->with([
          'products' => $products, 
          'tags' => $tags
          ]);
     }  
     
     /*
     * Increment product ID every time
     * a new product is added, and return
     * the ID to be used in product object
     */
      static function getProductId()  
      {  
          (!Redis::exists('product_count')) 
           Redis::set('product_count',0);  
           
          return Redis::incr('product_count');  
     }  
     
     /*
     * Create a hash map to hold a project object
     * e.g HMSET product:1 product "men jean" id 1 image "img-url.jpg" 
     * Then add the product ID to a list hold all products ID's
     */
     static function newProduct($productId, $data) : bool  
     {  
          self::addToProducts($productId);  
      
          return Redis::hMset("product:$productId", $data);  
     }  
     
     /*
     * A Ordered Set holding all products ID with the
     * PHP time() when the product was added as the score
     * This ensures products are listed in DESC when fetched
     */
     static function addToProducts($productId) : void  
     {  
          Redis::zAdd('products', time(), $productId);  
     } 
     
     /*
     * A unique Sets of tags
     */
     static function addToTags(array $tags)  
     {  
         Redis::sAddArray('tags',$tags);  
     }  
     
     /*
     * A unique set of tags for a particular product
     * eg SADD product:1:tags jean men pants 
     */
     static function addToProductTags($productId, $tags)  
     {  
         Redis::sAddArray("product:$productId:tags",$tags);  
     }  
     
     /*
     * A List of products carry this particular tag
     * ex1 RPUSH men 1 3
     * ex2 RPUSH women 2 4 
     */
     static function addProductToTags($productId, $tags)  
     {  
         foreach ($tags as $tag){  
          Redis::rPush($tag,$productId);  
         } 
     }  
     
    /*  
    * In a real live example, we will be returning 
    * paginated data by calling the lRange command 
    * lRange start end 
    */  
    static function getProducts($start = 0, $end = -1) : array  
    {  
          $productIds = Redis::zRange('products', $start, $end, true);  
          $products = [];  
      
          foreach ($productIds as $productId => $score) 
          {  
              $products[$score]= Redis::hGetAll("product:$productId");  
          } 
          
          return $products;  
     }
 
 }

Now if you visit in your browser /products/create you will be presented with a form to create new product.

To make it easier fo you, below is a list of 6 products with image hosted on s3 bucket and tags to get you started. We are using image url as oppose to uploading our own image as that will do for the purpose of his tutorial.

ProductImageTags
Sleeveless Jump Suitjump-suit.jpgwomen, jump suit
Men Jeanjean.jpgmen,pants,jean
Pencil Skirtpencil-skirt.jpgwomen,skirt
Men Sandalsandal.jpgmen,footwear,sandal
Smock Topsmock-top.jpgwomen,top
T-Shirttees.jpgmen,women,top

After adding all products depending on what order, you should get something like below sceenshot.

Redis & Laravel Ecommerce Demo

Filter By Tags

We want view all products relating to a tag when we click on the tag. To achive this add a new method to the ProductController

static function getProductByTags($tag, $start = 0, $end = -1) : array  
{  
  $productIds = Redis::lRange($tag, $start, $end);  
  $products = [];  
  
  foreach ($productIds as $productId) {  
  $products[] = Redis::hGetAll("product:$productId");  
 }  
  return $products;  
}

Update the viewProducts method to check if there is a tag URL param present.

public function viewProducts(Request $request)  
{  
  if($request->has('tag')){  
  $products = self::getProductByTags(($request->get('tag')));  
 }else{  
  $products = self::getProducts();  
 }  
  $tags = Redis::sMembers('tags');  
  
  return view('products.browse')->with(['products' => $products, 'tags' => $tags]);  
}

As you can see, there are many possibilty to what can be done with Redis as the database. As a task or more or less a challenge, I will leave you to display a product and under it display similar products under it.

Hint: Similar products share the same tags with the current displaying product.

Originally posted on https://www.oluwafemialofe.com/posts/using-redis-with-laravel-framework

Watch the video version of this course.

Comments
* The email will not be published on the website.