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
Requirements
For the purpose of this tutorial, you must have
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.
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.
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
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 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
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"
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 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"
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 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
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.
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.
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.
Product | Image | Tags |
---|---|---|
Sleeveless Jump Suit | jump-suit.jpg | women, jump suit |
Men Jean | jean.jpg | men,pants,jean |
Pencil Skirt | pencil-skirt.jpg | women,skirt |
Men Sandal | sandal.jpg | men,footwear,sandal |
Smock Top | smock-top.jpg | women,top |
T-Shirt | tees.jpg | men,women,top |
After adding all products depending on what order, you should get something like below sceenshot.
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.