Build a live search feature with Laravel and Vue.js

Introduction

Introduction

Today we’ll be creating a realtime search engine. Realtime means that our users will get updates as soon as they occur. So using our application, our users can search data and get results instantly. We’ll be using Laravel for the backend part, Vue.js for creating our dynamic user interface and Pusher Channels to get realtime updates.

Prerequisites

In order to follow this tutorial a basic or good understanding of Vue.js and Laravel is required, as we’ll be using these technologies throughout this tutorial. Also ensure you have npm or Yarn on your machine.

We’ll be using these tools to build our application:

Here’s a demo of the final product:

laravel-search-demo

Set up Pusher Channels

To get started with Pusher Channels, sign up for a free Pusher account. Then go to the dashboard and create a new Channels app. Retrieve your credentials from the API Keys tab, and make note of it as we’ll use them later in the tutorial.

Initializing the project and installing dependencies

To get started we’ll install a new Laravel application using the Laravel CLI. We’ll run the following command:

laravel new realtime_search_pusher

Once the installation is finished run the following command to move to your app directory:

cd realtime_search_pusher.

Now we’ll install our node dependencies, first paste this in your package.json file:

1{
2      "private": true,
3      "scripts": {
4        "dev": "npm run development",
5        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
6        "watch": "npm run development -- --watch",
7        "watch-poll": "npm run watch -- --watch-poll",
8        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
9        "prod": "npm run production",
10        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
11      },
12      "devDependencies": {
13        "axios": "^0.18",
14        "bootstrap": "^4.0.0",
15        "cross-env": "^5.1",
16        "jquery": "^3.2",
17        "laravel-mix": "^2.0",
18        "lodash": "^4.17.5",
19        "popper.js": "^1.12",
20        "vue": "^2.5.7",
21        "vuex": "^3.0.1",
22        "laravel-echo": "^1.4.0",
23        "pusher-js": "^4.2.2"
24      }
25    }

Then run npm install or yarn to install the dependencies. It’s up to you.

After this step, add the following to your .env file in the root of your project directory. Ensure to replace the placeholders with your keys from Pusher.

1PUSHER_APP_ID=YOUR_PUSHER_APP_ID
2    PUSHER_APP_KEY=YOUR_PUSHER_APP_KEY
3    PUSHER_APP_SECRET=YOUR_PUSHER_APP_SECRET
4    PUSHER_APP_CLUSTER=YOUR_PUSHER_APP_CLUSTER

Database setup

In this tutorial we’ll use SQLite as our database. Create a database.sqlite file in the database directory, and amend the .env file like this:

1DB_CONNECTION=sqlite
2    DB_DATABASE=/absolute/path/to/database.sqlite

Refer to this section on Laravel website for more relevant information.

Building models and seeding our database

Now, let’s build our database structure. We’ll use again Laravel CLI for that.
Run this command:

php artisan make:model Product -mc

The above command will generate the Product model as well as its migration and its controller ProductController.php for us.

Open your Product.php file and paste this:

1//realtime_search_pusher/app/Product.php
2    
3    <?php
4    
5    namespace App;
6    
7    use Illuminate\Database\Eloquent\Model;
8    
9    class Product extends Model {
10    
11        //
12        protected $fillable = ['name', 'description', 'price', 'image'];
13    }

Next copy and paste this piece of code in your product migration file:

1//realtime_search_pusher/database/migrations/*_create_products_table.php
2    
3    <?php
4    
5    use Illuminate\Database\Migrations\Migration;
6    use Illuminate\Database\Schema\Blueprint;
7    use Illuminate\Support\Facades\Schema;
8    
9    class CreateProductsTable extends Migration
10    {
11        /**
12         * Run the migrations.
13         *
14         * @return void
15         */
16        public function up()
17        {
18            Schema::create('products', function (Blueprint $table) {
19                $table->increments('id');
20                $table->string('name');
21                $table->integer('price');
22                $table->string('image');
23                $table->string('description');
24                $table->timestamps();
25            });
26        }
27    
28        /**
29         * Reverse the migrations.
30         *
31         * @return void
32         */
33        public function down()
34        {
35            Schema::dropIfExists('products');
36        }
37    }

Then run php artisan migrate to run the migration.

Now, we’ll seed our database to avoid having to populate it manually because it can be tedious.

Execute this command php artisan make:factory ProductFactory to generate a factory for our Product model. Next copy and paste the following code inside our ProductFactory.php file

1//realtime_search_pusher/database/factories/ProductFactory.php
2    <?php
3    
4    use App\Product;
5    use Faker\Generator as Faker;
6    
7    $factory->define(Product::class, function (Faker $faker) {
8        return [
9            'name'=> $faker->name,
10            'price' => $faker->numberBetween(25, 850),
11            'description' => $faker->text(100),
12            'image' => $faker->imageUrl(850, 640, 'food',true),
13            //
14        ];
15    });

Our ProductFactory.php sets a value for each of our Product model field as you can see in the above code.
The last step in this section is to tell Laravel to use our ProductFactory. Let’s do that, paste this code inside your DatabaseSeeder.php file:

1//realtime_search_pusher/database/seeds/DatabaseSeeder.php
2    <?php
3    
4    use Illuminate\Database\Seeder;
5    
6    class DatabaseSeeder extends Seeder
7    {
8        /**
9         * Seed the application's database.
10         *
11         * @return void
12         */
13        public function run()
14        {
15            factory(App\Product::class, 25)->create();
16        }
17    }

We generate 25 instances of our Product model. Finally run this command so that Laravel can seed the database with the factory we define: php artisan db:seed.

If you check your database, you can see that your database has been populated as well and it contains 25 rows. Great isn’t it!

Defining routes and creating the ProductController

In this section we’ll define our app endpoints and define the logic behind our ProductController.php .

Let’s create a get route named search (which will be called when the user attempts to search for products), and another get route named products to fetch our products from database. Paste the following into api.php:

1//realtime_search_pusher/routes/api.php
2    use Illuminate\Support\Facades\Route;
3    
4    Route::get('search','ProductController@search');
5    Route::get('products','ProductController@get');

We should also define a get route named / to return our app view. Copy this code and replace the existing one inside your web.php file:

1//realtime_search_pusher/routes/web.php
2    Route::get('/', function () {
3        return view('search');
4    });

Now let’s define our controller logic. Our controller functions will be responsible for actions to handle when some requests reach our API endpoints.

Open your ProductController file and paste the following code:

1//realtime_search_pusher/app/Http/Controllers/ProductController.php
2    
3    <?php
4    
5    namespace App\Http\Controllers;
6    
7    use App\Events\SearchEvent;
8    use App\Product;
9    use Illuminate\Http\Request;
10    
11    class ProductController extends Controller
12    {
13        //
14        public function search(Request $request)
15        {
16            $query = $request->query('query');
17            $products = Product::where('name', 'like', '%' . $query . '%')
18                ->orWhere('description', 'like', '%' . $query . '%')
19                ->get();
20    
21            //broadcast search results with Pusher channels
22            event(new SearchEvent($products));
23    
24            return response()->json("ok");
25        }
26    
27        //fetch all products
28        public function get(Request $request)
29        {
30            $products = Product::all();
31            return response()->json($products);
32        }
33    }

In the above code we have two functions get and search:

  • get - this function returns all existing posts in our database
  • search - this function is a bit tricky. It gets the query sent in the request and returns every product whose name or description contains it. This is handled there:
1Product::where('name', 'like', '%' . $query . '%')
2                ->orWhere('description', 'like', '%' . $query . '%')

Emit event

Well you may have noticed this line: event(new SearchEvent($products)). What is its purpose?
It broadcasts an event with search results to the client-side of our app using Laravel broadcasting. We’ll see how to create this event in the next part of the tutorial.

Create a search event with broadcasting

Broadcast is a fancy way to say emit.
Our SearchEvent event will be emitted whenever the user searches for a product or initiates a search. Enough talk , let’s focus on the code. Let’s create our SearchEvent by running the following command in your terminal: php artisan make:event SearchEvent.

Now open your SearchEvent file and paste the following:

1//realtime_search_pusher/app/Events/SearchEvent.php
2    
3    <?php
4    
5    namespace App\Events;
6    
7    use Illuminate\Broadcasting\Channel;
8    use Illuminate\Broadcasting\InteractsWithSockets;
9    use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
10    use Illuminate\Foundation\Events\Dispatchable;
11    use Illuminate\Queue\SerializesModels;
12    
13    class SearchEvent implements ShouldBroadcastNow
14    {
15        use Dispatchable, InteractsWithSockets, SerializesModels;
16    
17        /**
18         * Create a new event instance.
19         *
20         * @return void
21         */
22    
23        public $products;
24    
25        public function __construct($products)
26        {
27            //
28            $this->products = $products;
29        }
30    
31        /**
32         * Get the channels the event should broadcast on.
33         *
34         * @return \Illuminate\Broadcasting\Channel|array
35         */
36    
37        /**
38         * @return string
39         */
40        public function broadcastAs()
41        {
42            return 'searchResults';
43        }
44    
45        public function broadcastOn()
46        {
47            return new Channel('search');
48        }
49    }

Our class constructor initializes a set of products that are nothing but the search results from the database. We have two additional functions that may seem strange to you:

  • broadcastAs: customizes the broadcast name because by default Laravel uses the event’s class name.
  • broadcastOn: defines the channel search (which we’ll set up further on the tutorial) on which our event should be broadcast.

Broadcasting configuration

According to Laravel documentation about event broadcasting, before broadcasting any events, you will first need to register the App\Providers\BroadcastServiceProvider. In fresh Laravel applications, you only need to uncomment this provider in the providers array of your ../config/app.php configuration file. This provider will allow you to register the broadcast authorization routes and callbacks.

If this is done, you have to tell Laravel to use Pusher to broadcast events. Open your .env file and ensure you have this line: *BROADCAST_DRIVER*``=pusher

As we are broadcasting our events over Pusher, we should install the Pusher PHP SDK using the Composer package manager:

    composer require pusher/pusher-php-server "~3.0"

Setting up the broadcast channel

Laravel broadcasts events on well defined channels. As said above our event should be broadcast on search channel. It’s time to set it up. Paste the following code in your channels.php file:

1//realtime_search_pusher/routes/channels.php
2    
3    Broadcast::channel('search', function () {
4        return true;
5    });

As we aren’t using Laravel auth, we return true in the function callback so that everybody can use this channel to broadcast events.

Set up Laravel Echo

We’ll use Laravel Echo to consume our events on the client-side.

Open your resources/js/bootstrap.js file and uncomment this section of the code:

1import Echo from 'laravel-echo'
2    
3    window.Pusher = require('pusher-js');
4    
5    window.Echo = new Echo({
6        broadcaster: 'pusher',
7        key: process.env.MIX_PUSHER_APP_KEY,
8        cluster: process.env.MIX_PUSHER_APP_CLUSTER,
9        encrypted: false
10    });

The above code sets up Laravel Echo with Pusher Channels. This will make our app aware of events broadcasted, and Laravel Echo will consume our events

Our app is ready to broadcast and consume events in realtime using Pusher Channels. Let’s focus now on the frontend of your app.

Set up Vuex store

We’ll be using the Vuex library to centralize our data and control the way it is mutated throughout our application.

Create our state

Vuex state is a single object that contains all our application data. So let’s create ../resources/js/store/state.js and paste this code inside:

1let state = {
2        products: []
3    }
4    export default  state

The code above is straightforward. The products key is an array responsible to store our database products

Create our getters

With help of getters we can compute derived based on our data store state. Create ../resources/js/store/getters.js and paste this code inside

1let getters = {
2        products: state => {
3            return state.products
4        }
5    }
6    
7    export default getters

Create our mutations

Mutations allow us to perform some changes on our data. Create ../resources/js/store/mutations.js and paste this piece of code inside:

1let mutations = {
2        SET_PRODUCTS(state, products) {
3            state.products = products
4        }
5    }
6    
7    export default mutations

Our mutations object has a SET_PRODUCTS function with two arguments state and products; this function assigns the products array to our state products key.

Create our actions

Vuex actions allow us to perform asynchronous operations over our data. Create the file ../resources/js/store/actions.js and paste the following code:

1let actions = {
2        SEARCH_PRODUCTS({commit}, query) {
3            let params = {
4                query
5            };
6            axios.get(`/api/search`, {params})
7                .then(res => {
8                    if (res.data === 'ok')
9                        console.log('request sent successfully')
10    
11                }).catch(err => {
12                console.log(err)
13            })
14        },
15        GET_PRODUCTS({commit}) {
16            axios.get('/api/products')
17                .then(res => {
18                    {
19                        commit('SET_PRODUCTS', res.data)
20                    }
21                })
22                .catch(err => {
23                    console.log(err)
24                })
25        }
26    }
27    
28    export default actions

We have defined two actions and each of them responsible of a single operation, either products search or products search. They both perform asynchronous calls to our API routes.

SEARCH_PRODUCTS sends a get request to our /api/search endpoint to get products. This action is dispatched whenever the user is searching for something.

GET_PRODUCTS makes a get request to our api/products endpoint to get our database products and commits the request result with SET_PRODUCTS mutation.

Set up our store with Vue

Create the file ../resources/js/store/index.js and paste this code inside:

1import Vue from 'vue'
2    import Vuex from 'vuex'
3    import actions from './actions'
4    import mutations from './mutations'
5    import getters from './getters'
6    import state from "./state";
7    
8    Vue.use(Vuex);
9    
10    export default new Vuex.Store({
11        state,
12        mutations,
13        getters,
14        actions
15    })

Then, we export our store and add it to the Vue instance. Add this code to your ../resouces/js/app.js file.

1require('./bootstrap');
2    window.Vue = require('vue');
3    
4    import store from './store/index'
5    
6    Vue.component('searchbar', require('./components/Searchbar'));
7    Vue.component('product', require('./components/Product'))
8    Vue.component('products', require('./components/Products'))
9    
10    const app = new Vue({
11        el: '#app',
12        store
13    });

The previous code also globally registers three Vue components, Searchbar.vue ,Product.vue and Products.vue that we’ll build in the next part of this tutorial.

Building Vue components

We’ll build three Vue components for our app, a Searchbar component, a Product component and a Products component, each of them responsible for a single functionality.

Create the Product.vue component

The Product.vue component is responsible for encapsulating details about a single product instance from the database and rendering it in a proper and styled way.
Paste the following inside your Product.vue component.

1//../resources/js/components/Product.vue
2    
3    <template>
4        <div class="card">
5            <img class="card-img-top" :src="product.image" :alt="product.name">
6            <div class="card-body">
7                <h5 class="card-title">{{product.name}}</h5>
8                <p class="card-text">{{product.description}}</p>
9                <span class="text-danger font-weight-bold">${{product.price}}</span>
10            </div>
11        </div>
12    </template>
13    
14    <script>
15        export default {
16            name: "Product",
17            props: ['product']
18        }
19    </script>
20    
21    <style scoped>
22        .card {
23            cursor: pointer;
24            margin-bottom: 8px;
25        }
26    </style>

Our Product.vue component takes a product property whose details we render in the component body. This code is straightforward as you may have noticed it. We are using the Bootstrap framework to style our component.

Create the Products.vue component

This component will render products items from database. It’s that simple.
So create your Products.vue component and paste this code inside:

1//../resources/js/components/Products.vue
2    
3    <template>
4        <div class="container">
5            <div class="row" v-for="products in groupedProducts">
6                <div class="col-md-3 col-sm-6" v-for="product in products">
7                    <product class="animated fadeIn" :product="product"></product>
8                </div>
9                <div class="col w-100"></div>
10            </div>
11        </div>
12    </template>
13    
14    <script>
15        import {mapGetters} from 'vuex'
16        import product from '../components/Product'
17    
18        export default {
19            name: "Products",
20            components: {
21                product
22            },
23            mounted() {
24                this.$store.dispatch('GET_PRODUCTS')
25    
26                window.Echo.channel('search')
27                    .listen('.searchResults', (e) => {
28                        this.$store.commit('SET_PRODUCTS', e.products)
29                    })
30    
31            },
32            computed: {
33                groupedProducts() {
34                    return _.chunk(this.products, 4);
35                },
36                ...mapGetters([
37                    'products'
38                ])
39            }
40        }
41    </script>

This component has a groupedProducts computed property which splits our products array and makes group of six.

In the mounted hook function we dispatch the GET_PRODUCTS action, and we use Vuex helper function …mapGetters() to access our products state.

We also use the Echo library here to listen to events.

1window.Echo.channel('search')
2                    .listen('.searchResults', (e) => {
3                        this.$store.commit('SET_PRODUCTS', e.products)
4    
5                        console.log(e)
6                    })

We first subscribe to search channel, then we listen to the searchResults event triggered when the user searches for something. Then we commit SET_PRODUCTS mutation with the event payload. Yes it’s that simple.

Note: You must append a dot to the event name so Laravel Echo can listen to your event.
Otherwise you won’t be able to listen to any event.

Create our Searchbar.vue component

This component contains the input field with a search Button. Whenever the user enters their search query inside the input field, we’ll make a request to our backend to get realtime results.

Copy and paste the following code in your Searchbar.vue component:

1<template>
2        <div class="container">
3            <div class="input-group mb-3">
4                <input v-model="query" type="text" class="form-control" placeholder="Product name or description"
5                       aria-label="Product name or description"
6                       aria-describedby="basic-addon2">
7                <div class="input-group-append">
8                    <button class="btn btn-primary" @click="searchProducts" @keyup.enter="searchProducts" type="button">
9                        Search
10                    </button>
11                </div>
12            </div>
13        </div>
14    </template>
15    
16    <script>
17        export default {
18            name: "Searchbar",
19            data() {
20                return {
21                    query: '',
22                }
23            },
24            watch: {
25                query: {
26                    handler: _.debounce(function () {
27                        this.searchProducts()
28                    }, 100)
29                }
30            },
31            methods: {
32                searchProducts() {
33                    this.$store.dispatch('SEARCH_PRODUCTS', this.query)
34                }
35            }
36        }
37    </script>
38    
39    <style scoped>
40    
41    </style>

We bind the query data to our input field using Vue.js v-model, and we watch it. Whenever the query data changes we wait 100 ms and dispatch SEARCH_PRODUCTS action. This is what the following code is doing:

1query: {
2                    handler: _.debounce(function () {
3                        this.searchProducts()
4                    }, 100)
5                }

Finalize the app

Now, let’s create our search.blade.php file which contains our three Vue components. Paste this code inside:

1//../resources/views/search.blade.php
2    
3    <!doctype html>
4    <html lang="en">
5    <head>
6        <meta charset="UTF-8">
7        <meta name="viewport"
8              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
9        <meta http-equiv="X-UA-Compatible" content="ie=edge">
10    
11        <meta name="csrf-token" content="{{ csrf_token() }}">
12    
13        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
14              integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
15    
16        <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
17        <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
18        {{--bxslider--}}
19    
20        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css"/>
21    
22        <link rel="stylesheet" href="{{mix('css/app.css')}}">
23        <title>Realtime search engine with Pusher and Laravel</title>
24    </head>
25    <body>
26    
27    
28    <div id="app">
29    
30        <div class="container">
31            <h5 class="text-center" style="margin-top: 32px">Realtime search engine with Laravel and Pusher</h5>
32            <br><br>
33            <searchbar></searchbar>
34            <products></products>
35        </div>
36    
37    </div>
38    
39    <script async src="{{mix('js/app.js')}}"></script>
40    
41    </body>
42    
43    </html>

We are almost done. Now open your terminal and run npm run dev to build your app in a proper way. This can take a few seconds. After this step run php artisan serve and open your browser at localhost:8000 to see your app working fine. Now try searching a product name or its description in the searchbar, you should get realtime results for your search. You are now a boss 😎

laravel-search-demo

NOTE: If you encounter a 500 error when trying to search, it’s possible that you have to disable Pusher encryption. Open these files ../config/broadcasting.php and ../resources/js/bootstrap.js and make sure you disable Pusher encryption encrypted: false in both of them.

Conclusion

In this tutorial we’ve created a realtime search engine using Laravel, Vue.js, and Pusher Channels to provide realtime functionality. You can think up new ideas to extend the application. It’ll be fun to see what you come up with. The source code for this tutorial is available on GitHub here.