Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P3)

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P3)

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (Part 3): Background WebSocket Server Implementation

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (Part 3): Background WebSocket Server Implementation

Today we continue the development of the background function of the chat room project. Now that the back-end database is ready and the user authentication function based on API Token has been implemented, next, let's implement the core of the chat room function-the realization of the business logic of the WebSocket server.

Create WebSocketHandler

First we create WebSocketHandler.php in the app/services directory to handle WebSocket communication, and initialize the WebSocketHandler class code as follows:

<? php
namespace App \ Services;

use Hhxsv5 \ LaravelS \ Swoole \ WebSocketHandlerInterface;
use Illuminate \ Support \ Facades \ Log;
use Swoole \ Http \ Request;
use Swoole \ WebSocket \ Frame;
use Swoole \ WebSocket \ Server;

class WebSocketHandler implements WebSocketHandlerInterface
{
    public function __construct ()
    {
        // The constructor cannot be omitted even if it is empty
    }

    // Triggered when the connection is established
    public function onOpen (Server $server, Request $request)
    {
        Log :: info ('WebSocket connection established:'. $Request-> fd);
    }

    // Triggered when a message is received
    public function onMessage (Server $server, Frame $frame)
    {
        // $frame-> fd is the client id and $frame-> data is the data sent by the client
        Log :: info ("Data received from {$frame-> fd}: {$frame-> data}");
        foreach ($server-> connections as $fd) {
            if (! $server-> isEstablished ($fd)) {
                // Ignore if connection is not available
                continue;
            }
            $server-> push ($fd, $frame-> data); // The server broadcasts messages to all clients through the push method
        }
    }

    // Triggered when the connection is closed
    public function onClose (Server $server, $fd, $reactorId)
    {
        Log :: info ('WebSocket connection closed:'. $Fd);
    }
}

This class implements WebSocketHandlerInterface an interface, it must implement the interface conventions of constructors onOpen, onMessage and onClose methods, specific features of each method in front of our integrated Swoole achieve the WebSocket server Laravel has been introduced, in establishing connections and disconnections, we only print a log record, upon receipt of the message from the client, we will record the message and broadcast to each client connected to the server WebSocket (logically defined in the corresponding onMessage method).

Asynchronous event monitoring and processing

Of course, when we build a chat room project, the business functions we implement are more complex than the barrage functions we implemented before . In addition to broadcasting the message to all clients, we also need to save the message to the database and verify whether the user Logged in, unlogged users cannot send messages. Let's first handle the implementation of saving messages to the database.

Since operating the database is a time-consuming operation involving network IO, here we transfer it to the Task Worker for processing through the asynchronous event monitoring mechanism provided by Swoole , thereby improving the communication performance of the WebSocket server.

First, create a message receive event with the help of Laravel's Artisan command MessageReceived:

php artisan make:event MessageReceived

Then modify the generated app/Events/MessageReceived.php code is as follows:

<?php
namespace App\Events;

use App\Message;
use Hhxsv5\LaravelS\Swoole\Task\Event;

class MessageReceived extends Event
{
    private $message;
    private $userId;

    /**
     * Create a new event instance.
     */
    public function __construct($message, $userId = 0)
    {
        $this->message = $message;
        $this->userId = $userId;
    }

    /**
     * Get the message data
     * 
     * return App\Message
     */
    public function getData()
    {
        $model = new Message();
        $model->room_id = $this->message->room_id;
        $model->msg = $this->message->type == 'text' ? $this->message->content : '';
        $model->img = $this->message->type == 'image' ? $this->message->image : '';
        $model->user_id = $this->userId;
        $model->created_at = Carbon::now();
        return $model;
    }
}

This event class only for incoming data format conversion, here we incoming messages from external objects and the user ID, then getData it is a method of combination Message of model instances and returns.

Since the Message model contains only a creation time, the update does not include the time, so we explicitly specified created_at field, while also the Message model class $timestamps property is set false to avoid the system automatically set the time for its field.

class Message extends Model
{
    public $timestamps = false;
}

Then, create a message listener MessageListener for the above MessageReceived events are processed:

php artisan make:listener MessageListener

Modify the latest generation of app/Listeners/MessageListener.php file code is as follows:

<?php

namespace App\Listeners;

use App\Events\MessageReceived;
use Hhxsv5\LaravelS\Swoole\Task\Listener;
use Illuminate\Support\Facades\Log;

class MessageListener extends Listener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  MessageReceived  $event
     * @return void
     */
    public function handle($event)
    {
        $message = $event->getData();
        Log::info(__CLASS__ . ': Start Processing', $message->toArray());
        if ($message && $message->user_id && $message->room_id && ($message->msg || $message->img)) {
            $message->save();
            Log::info(__CLASS__ . ': Processed');
        } else {
            Log::error(__CLASS__ . ': The message field is missing and cannot be saved');
        }
    }
}

In the message listener, by handle a method of handling an event, the incoming $event parameters corresponding to the above-described MessageReceived object instance, and then we message data stored in the checksum method while the corresponding print log information.

User authentication checksum message trigger event

With the message after message received event and event listeners, then, we need to trigger the WebSocket server receives a message receiving event, the business logic in WebSocketHandler the onMessage process is complete, modify onMessage codes are as follows:

use App\Events\MessageReceived;
use Hhxsv5\LaravelS\Swoole\Task\Event; 
use App\User;

// Triggered when a message is received
public function onMessage(Server $server, Frame $frame)
{
    // $frame->fd is the client id,$frame->data is the data send by the client
    Log::info("From {$frame->fd} Received data: {$frame->data}");
    $message = json_decode($frame->data);
    // Based on Token user authentication check
    if (empty($message->token) || !($user = User::where('api_token', $message->token)->first())) {
        Log::warning("User" . $message->name . "is offline, cannot send message");
        $server->push($frame->fd, "User offline cannot send message");  // Inform users that offline status cannot send messages
    } else {
        // Trigger message receive event
        event = new MessageReceived($message, $user->id);
        Event::fire($event);
        unset($message->token);  // Remove the current user token field from the message
        foreach ($server->connections as $fd) {
            if (!$server->isEstablished($fd)) {
                // Ignored if connection is not available
                continue;
            }
            $server->push($fd, json_encode($message)); // The server passes push method to send data to all connected clients
        }
    }
}

Here, we first received data is decoded (assuming the client is passed over JSON string), and determines if it contains token the fields, and the tokenvalue is valid, as a basis and determines whether the user is authenticated, for no Authenticated users do not broadcast messages to other clients, but simply inform the user that they need to log in to send messages. Conversely, if the user has logged in, trigger MessageReceived events, and objects incoming messages and user ID, and then save the follow-up process by the message listener, and then through all the WebSocket server build effective client connections, and remove the Token field Messages are broadcast to them, completing one-time sending of chat messages.

NOTE: WebSocket HTTP connection authentication before the connection using a different connection, the authentication logic is independent of, not simply by Auth determining that way, that only applies to a logical HTTP communication.

User authentication logic adjustment

To this end, we must adjust the default user authentication logic Token when a user registration is successful or login is successful, it will update the users list of api_token field values, when the user exits, then empty the field value, corresponding to implement custom code in app/Http/Controllers/AuthController.php the:

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api')->only('logout');
    }

    public function register(Request $request) 
    {
        // Verify registration fields
        Validator::make($request->all(), [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:6']
        ])->validate();

        // Create user in database and return
        return User::create([
            'name' => $request->input('name'),
            'email' => $request->input('email'),
            'password' => Hash::make($request->input('password')),
            'api_token' => Str::random(60)
        ]);
    }

    public function login(Request $request) 
    {
        // Verify login fields
        $request->validate([
            'email' => 'required|string',
            'password' => 'required|string',
        ]);

        $email = $request->input('email');
        $password = $request->input('password');
        $user = User::where('email', $email)->first();
        // Token information is returned if the user successfully checks
        if ($user && Hash::check($password, $user->password)) {
            $user->api_token = Str::random(60);
            $user->save();
            return response()->json(['user' => $user, 'success' => true]);
        }

        return  response()->json(['success' => false]);
    }

    public function logout(Request $request) 
    {
        $user = Auth::guard('auth:api')->user();
        $userModel = User::find($user->id);
        $userModel->api_token = null;
        $userModel->save();
        return response()->json(['success' => true]);
    }
}
WebSocket server and asynchronous event listening configuration

Finally, we modify config/laravels.php the configuration file, complete WebSocket server configuration, and asynchronous event listeners.

By configuring the first websocket promoter and define WebSocket communication processor:

'websocket'                => [
    'enable' => true,
    'handler' => \App\Services\WebSocketHandler::class,
],

I.e., asynchronous event listener corresponding to mapping in the events configuration item configuration, an event can be processed and a plurality of listeners listen:

'events'                   => [
    \App\Events\MessageReceived::class => [
        \App\Listeners\MessageListener::class,
    ]
],

Also, listen for and handle asynchronous events is the Task Worker process by Swoole process, so it needs to open task_worker_num the configuration, here we use the default configuration to:

'swoole'                   => [
    ...
    'task_worker_num'    => function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8,
    ...
]

For Laravel applications running on the Swoole HTTP server, because the Laravel container will reside in memory, when it comes to user authentication, it is necessary to clear the authentication status of this request after each request, so as not to be used by other users. profile laravels.php of cleaners configuration items can be uncommented before this line configuration is as follows:

'cleaners'                 => [
    ...
    \Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,    // If you use the authentication or passport in your project, please uncomment this line
    ...
],

Finally, we .env add the following two lines of configuration, respectively, for IP address assignment Swoole HTTP / WebSocket server is running and whether or not running in the background:

LARAVELS_LISTEN_IP=workspace
LARAVELS_DAEMONIZE=true

At this point, the background WebSocket server coding and configuration work of the Laravel project has been completed. Next, we will configure Nginx so that the WebSocket service can provide external access.

Nginx virtual host configuration

Taking the Laradock workbench as an example, a new virtual host webchats.conf( laradock/nginx/sites under the directory) is added to Nginx to configure support for the Swoole HTTP server and WebSocket server (Swoole's WebSocket server is based on the HTTP server, so both need to be configured at the same time):

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream webchats {
    # Connect IP:Port
    server workspace:5200 weight=5 max_fails=3 fail_timeout=30s;
    keepalive 16;
}

server {
    listen 80;

    server_name webchats.test;
    root /var/www/webchat/public;

    error_log /var/log/nginx/webchats_error.log;
    access_log /var/log/nginx/webchats_access.log;

    autoindex off;
    index index.html index.htm;

    # Nginx handles the static resources(recommend enabling gzip), LaravelS handles the dynamic resource.
    location / {
        try_files $uri @webchats;
    }

    # Response 404 directly when request the PHP file, to avoid exposing public/*.php
    #location ~* \.php$ {
    #    return 404;
    #}

    # Http and WebSocket are concomitant, Nginx identifies them by "location"
    # !!! The location of WebSocket is "/ws"
    # Javascript: var ws = new WebSocket("ws://webchats.test/ws");
    # Handle WebSocket communication
    location ^~ /ws/ {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout: Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds; At the same time, this close behavior is also affected by heartbeat setting of Swoole.
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://webchats;
    }

    location @webchats {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_pass http://webchats;
    }
}

In local hosts mapping configuration file webchats.test domain:

127.0.0.1 webchats.test

Next, restart the Nginx container:

docker-compose up -d nginx
Restart Swoole HTTP server for verification

In laradock the directory by docker exec -it laradock_workspace_1 bash entering workspace the container, and then into the webchat directory, restart Swoole HTTP/WebSocket Server:

php bin/laravels restart

As a result, we can webchats.test access the application:

Successful access indicates that the Laravel application configuration based on the Swoole HTTP server is successfully configured, but the WebSocket communication and user authentication functions implemented in this tutorial have not been verified and tested. We will start writing the front-end page components in the next tutorial, and Use page automation tests to verify that the backend services are functioning properly.

Full Stack Vue.js & Laravel - How to use Vuejs Service Container Package

Full Stack Vue.js & Laravel - How to use Vuejs Service Container Package

In this full stack vue.js to Laravel tutorial explains how to use of the vuejs service container package. Laravel to Vue.js ~ Full Stack Vue.js & Laravel Integration (Vuejs Service Container Package)

In this full stack vuejs to laravel tutorial, we cover a wide array of topics ~ all of which circle around the use of the vuejs service container package (https://github.com/zhorton34/vue-service-container).

Order of topics we'll cover:
1: Installing the vue service container
2: Creating the vuejs service container
3: Injecting Php data into vue service container, utilizing laravel blade
4: Accessing the php content from within vuejs service container
5: Creating vuejs service providers (boot, register, afterLaunch, & when hooks - context object)
6. Creating a global Vue component within the welcome-page.js ServiceProvider
6: Laravel blade templates & vuejs templates within a single template
7. Configuring webpack aliases using webpack.mix.js
8: Implementing vuex store within service provider
9: Committing php data to vuex store BEFORE we mount our Vue instance to the DOM
10: Global Event Bus Service Provider
11: Filters vuejs Service Provider
12: Directives vuejs Service Provider
13: Implementing vue-router using a vuejs Service Provider
14: afterLaunch, hook into vue-router example
15: Code splitting (definition)
16: Code splitting (purpose)
17: Full Stack Laravel and vuejs code splitting implementation
18: Closing Thoughts

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P1)

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P1)

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (1): Environment Preparation

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (Part 1): Environment Preparation

Project Overview

Starting today, the academy will lead you to build a "big project" based on Swoole-develop online chat rooms. Of course, it is still in the Laravel framework, and the front-end UI is based on Vue components. This project mainly involves the following technologies:

  • Demonstrate and test based on Laradock's local development environment
  • Backend based on Laravel 5.8 + LaravelS extension package introduces support for Swoole
  • Chat communication based on WebSocket server function provided by Swoole
  • Combine Swoole's asynchronous tasks and coroutines to improve system response speed and performance
  • Use Redis + MySQL as a data storage medium
  • Implement user registration and login function, users can enter the chat room after login
  • Support text + picture + expression + link message
  • Support viewing historical messages and offline messages
  • Support chat with ordinary users / bots
  • The front end implements the UI based on the Vue.js framework, and introduces Vuex and Vue-Router to achieve front-end and back-end separation
  • Supports automated testing of projects with Laravel Dusk

The screenshot of the chat room finally implemented is shown below:

Note: The front-end UI of this project is borrowed from the front-end project https://github.com/hua1995116/webchat

Development environment

The development of the entire project is based on Laradock. Of course, you can also choose other integrated development environments, such as Homestead, Valet, Laragon. Wait, but this series of tutorials are based on Laradock by default. The subsequent project environment configuration, tool installation, debugging and running are subject to Laradock. Please use other development tools to adjust according to your own environment.

After completing the installation and initialization Laradock environment, the need in the root directory .env to enable NPM configuration file:

WORKSPACE_INSTALL_NODE=true
WORKSPACE_NODE_VERSION=node
WORKSPACE_NPM_REGISTRY=http://registry.npm.taobao.org/

And Swoole related configuration:

WORKSPACE_INSTALL_SWOOLE=true
...
PHP_FPM_INSTALL_SWOOLE=true

Then rebuild workspace and php-fpm mirrors:

docker-compose build workspace php-fpm

Let the above changes to take effect, and in workspace and php-fpm install Swoole expansion vessel. If you haven't started Nginx, Redis, MySQL, now run them as follows (the following tutorials assume that they are all started):

docker-compose up -d nignx mysql redis

Project initialization

Note: You can perform the following actions (requires PHP, Composer, Node.js and other local software installed), you can also enter a local workspace vessel operating under the specified directory.

Next, we installed and initialized chat room project webchat, because we were .env configured APP_CODE_PATH_HOST to point to the laradock same level wwwroot, so we can locally wwwroot to create the project directory by Composer:

composer create-project laravel/laravel webchat --prefer-dist -vvv

After successful creation, enter the project directory, install LaravelS extension package through Composer , in order to quickly use the API provided by Swoole for programming in Laravel project, and then publish the configuration file and script file of the extension package to the project root directory:

cd webchat
composer require hhxsv5/laravel-s
php artisan laravels publish

Then we enter the workspace vessel running php bin/laravels start start Swoole, verify Swoole environment is ready:

Seeing the above information indicates that the Swoole operating environment is normal.

Next, since we may use Redis in the future, install the corresponding extension package first:

composer require predis/predis

Environment configuration backend to come to an end, we next come to the front, running npm install initialize the front-dependent, execute the following command to install Vuex and Vue-Router after initialization is complete:

npm install vuex vue-router --save-dev

In this way, we have completed all the initialization of the chat room project. If other dependencies are needed during the subsequent development, we will install them in the specific tutorial.

automated test

We can also write browser test code for the project to implement automated testing. This can be achieved through the Dusk extension package provided by the Laravel framework. We can install this extension package through Composer:

composer require --dev laravel/dusk

Since we only need to be tested in the development environment, so the added --dev options. After running php artisan dusk:install in tests the directory browser initialization related test sample files, this command will download a file adapter ChromeDriver current platform expansion pack to the bindirectory, this time may experience network problems if the download fails:

However, since the Dusk extension package already comes with ChromeDriver files for each platform, you can ignore this error:

Next, we set up a virtual domain name for the project webchat.test, and in the root directory of Laradock nginx/sites new subdirectory corresponding virtual host configuration webchat.conf:

server {

    listen 80;
    listen [::]:80;

    server_name webchat.test;
    root /var/www/webchat/public;
    index index.php index.html index.htm;

    location / {
         try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        try_files $uri /index.php =404;
        fastcgi_pass php-upstream;
        fastcgi_index index.php;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        #fixes timeouts
        fastcgi_read_timeout 600;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt/;
        log_not_found off;
    }

    error_log /var/log/nginx/webchat_error.log;
    access_log /var/log/nginx/webchat_access.log;
}

Then the local host /etc/hosts mapping this domain name:

webchat.test 127.0.0.1

Finally restart Nginx in the Laradock root directory:

docker-compose up -d nginx

At this point, we can have http://webchat.test access to a chat room project.

Back to the webchat project, in .env modification APP_URL to the configuration of our domain names:

APP_URL=http://webchat.test

Then run php artisan dusk and test passes, it means that our Dusk test environment is also OK:

At this point, we have completed the preparation of the chat room project development environment, project initialization and test environment, and then we can start project development.

Build a live search with laravel and Vuejs

Build a live search with laravel and Vuejs

How to use Laravel and Vue.js to build a live search feature. In order to follow this tutorial a basic or good understanding of Vue.js

How to use Laravel and Vue.js to build a live search feature. In order to follow this tutorial a basic or good understanding of Vue.js.

Search with Laravel and Vue

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:

Pusher setup

Head over to the Pusher website and sign up for a free account. Select Create new app on the sidebar, and hit Create my app to create a new app after filling the form.

Once your app is created, 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:

    {
      "private": true,
      "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch",
        "watch-poll": "npm run watch -- --watch-poll",
        "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",
        "prod": "npm run production",
        "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"
      },
      "devDependencies": {
        "axios": "^0.18",
        "bootstrap": "^4.0.0",
        "cross-env": "^5.1",
        "jquery": "^3.2",
        "laravel-mix": "^2.0",
        "lodash": "^4.17.5",
        "popper.js": "^1.12",
        "vue": "^2.5.7",
        "vuex": "^3.0.1",
        "laravel-echo": "^1.4.0",
        "pusher-js": "^4.2.2"
      }
    }

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.

    PUSHER_APP_ID=YOUR_PUSHER_APP_ID
    PUSHER_APP_KEY=YOUR_PUSHER_APP_KEY
    PUSHER_APP_SECRET=YOUR_PUSHER_APP_SECRET
    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:

    DB_CONNECTION=sqlite
    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:

    //realtime_search_pusher/app/Product.php

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Product extends Model {

        //
        protected $fillable = ['name', 'description', 'price', 'image'];
    }

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

    //realtime_search_pusher/database/migrations/*_create_products_table.php

    <?php

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

    class CreateProductsTable extends Migration
    {
        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
            Schema::create('products', function (Blueprint $table) {
                $table->increments('id');
                $table->string('name');
                $table->integer('price');
                $table->string('image');
                $table->string('description');
                $table->timestamps();
            });
        }

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

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

    //realtime_search_pusher/database/factories/ProductFactory.php
    <?php

    use App\Product;
    use Faker\Generator as Faker;

    $factory->define(Product::class, function (Faker $faker) {
        return [
            'name'=> $faker->name,
            'price' => $faker->numberBetween(25, 850),
            'description' => $faker->text(100),
            'image' => $faker->imageUrl(850, 640, 'food',true),
            //
        ];
    });

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:

    //realtime_search_pusher/database/seeds/DatabaseSeeder.php
    <?php

    use Illuminate\Database\Seeder;

    class DatabaseSeeder extends Seeder
    {
        /**
         * Seed the application's database.
         *
         * @return void
         */
        public function run()
        {
            factory(App\Product::class, 25)->create();
        }
    }

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:

    //realtime_search_pusher/routes/api.php
    use Illuminate\Support\Facades\Route;

    Route::get('search','[email protected]');
    Route::get('products','[email protected]');

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:

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

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:

    //realtime_search_pusher/app/Http/Controllers/ProductController.php

    <?php

    namespace App\Http\Controllers;

    use App\Events\SearchEvent;
    use App\Product;
    use Illuminate\Http\Request;

    class ProductController extends Controller
    {
        //
        public function search(Request $request)
        {
            $query = $request->query('query');
            $products = Product::where('name', 'like', '%' . $query . '%')
                ->orWhere('description', 'like', '%' . $query . '%')
                ->get();

            //broadcast search results with Pusher channels
            event(new SearchEvent($products));

            return response()->json("ok");
        }

        //fetch all products
        public function get(Request $request)
        {
            $products = Product::all();
            return response()->json($products);
        }
    }

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:
    Product::where('name', 'like', '%' . $query . '%')
                ->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:

    //realtime_search_pusher/app/Events/SearchEvent.php

    <?php

    namespace App\Events;

    use Illuminate\Broadcasting\Channel;
    use Illuminate\Broadcasting\InteractsWithSockets;
    use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
    use Illuminate\Foundation\Events\Dispatchable;
    use Illuminate\Queue\SerializesModels;

    class SearchEvent implements ShouldBroadcastNow
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;

        /**
         * Create a new event instance.
         *
         * @return void
         */

        public $products;

        public function __construct($products)
        {
            //
            $this->products = $products;
        }

        /**
         * Get the channels the event should broadcast on.
         *
         * @return \Illuminate\Broadcasting\Channel|array
         */

        /**
         * @return string
         */
        public function broadcastAs()
        {
            return 'searchResults';
        }

        public function broadcastOn()
        {
            return new Channel('search');
        }
    }

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:

    //realtime_search_pusher/routes/channels.php

    Broadcast::channel('search', function () {
        return true;
    });

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:

    import Echo from 'laravel-echo'

    window.Pusher = require('pusher-js');

    window.Echo = new Echo({
        broadcaster: 'pusher',
        key: process.env.MIX_PUSHER_APP_KEY,
        cluster: process.env.MIX_PUSHER_APP_CLUSTER,
        encrypted: false
    });

The above code sets up Laravel Echo with Pusher. 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:

    let state = {
        products: []
    }
    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

    let getters = {
        products: state => {
            return state.products
        }
    }

    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:

    let mutations = {
        SET_PRODUCTS(state, products) {
            state.products = products
        }
    }

    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:

    let actions = {
        SEARCH_PRODUCTS({commit}, query) {
            let params = {
                query
            };
            axios.get(`/api/search`, {params})
                .then(res => {
                    if (res.data === 'ok')
                        console.log('request sent successfully')

                }).catch(err => {
                console.log(err)
            })
        },
        GET_PRODUCTS({commit}) {
            axios.get('/api/products')
                .then(res => {
                    {
                        commit('SET_PRODUCTS', res.data)
                    }
                })
                .catch(err => {
                    console.log(err)
                })
        }
    }

    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:

    import Vue from 'vue'
    import Vuex from 'vuex'
    import actions from './actions'
    import mutations from './mutations'
    import getters from './getters'
    import state from "./state";

    Vue.use(Vuex);

    export default new Vuex.Store({
        state,
        mutations,
        getters,
        actions
    })

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

    require('./bootstrap');
    window.Vue = require('vue');

    import store from './store/index'

    Vue.component('searchbar', require('./components/Searchbar'));
    Vue.component('product', require('./components/Product'))
    Vue.component('products', require('./components/Products'))

    const app = new Vue({
        el: '#app',
        store
    });

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.

    //../resources/js/components/Product.vue

    <template>
        <div class="card">
            <img class="card-img-top" :src="product.image" :alt="product.name">
            <div class="card-body">
                <h5 class="card-title">{{product.name}}</h5>
                <p class="card-text">{{product.description}}</p>
                <span class="text-danger font-weight-bold">${{product.price}}</span>
            </div>
        </div>
    </template>

    <script>
        export default {
            name: "Product",
            props: ['product']
        }
    </script>

    <style scoped>
        .card {
            cursor: pointer;
            margin-bottom: 8px;
        }
    </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:

    //../resources/js/components/Products.vue

    <template>
        <div class="container">
            <div class="row" v-for="products in groupedProducts">
                <div class="col-md-3 col-sm-6" v-for="product in products">
                    <product class="animated fadeIn" :product="product"></product>
                </div>
                <div class="col w-100"></div>
            </div>
        </div>
    </template>

    <script>
        import {mapGetters} from 'vuex'
        import product from '../components/Product'

        export default {
            name: "Products",
            components: {
                product
            },
            mounted() {
                this.$store.dispatch('GET_PRODUCTS')

                window.Echo.channel('search')
                    .listen('.searchResults', (e) => {
                        this.$store.commit('SET_PRODUCTS', e.products)
                    })

            },
            computed: {
                groupedProducts() {
                    return _.chunk(this.products, 4);
                },
                ...mapGetters([
                    'products'
                ])
            }
        }
    </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.

    window.Echo.channel('search')
                    .listen('.searchResults', (e) => {
                        this.$store.commit('SET_PRODUCTS', e.products)

                        console.log(e)
                    })

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:

    <template>
        <div class="container">
            <div class="input-group mb-3">
                <input v-model="query" type="text" class="form-control" placeholder="Product name or description"
                       aria-label="Product name or description"
                       aria-describedby="basic-addon2">
                <div class="input-group-append">
                    <button class="btn btn-primary" @click="searchProducts" @keyup.enter="searchProducts" type="button">
                        Search
                    </button>
                </div>
            </div>
        </div>
    </template>

    <script>
        export default {
            name: "Searchbar",
            data() {
                return {
                    query: '',
                }
            },
            watch: {
                query: {
                    handler: _.debounce(function () {
                        this.searchProducts()
                    }, 100)
                }
            },
            methods: {
                searchProducts() {
                    this.$store.dispatch('SEARCH_PRODUCTS', this.query)
                }
            }
        }
    </script>

    <style scoped>

    </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:

    query: {
                    handler: _.debounce(function () {
                        this.searchProducts()
                    }, 100)
                }
Finalize the app

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

    //../resources/views/search.blade.php

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">

        <meta name="csrf-token" content="{{ csrf_token() }}">

        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
              integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">

        <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
        <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
        {{--bxslider--}}

        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css"/>

        <link rel="stylesheet" href="{{mix('css/app.css')}}">
        <title>Realtime search engine with Pusher and Laravel</title>
    </head>
    <body>

    <div id="app">

        <div class="container">
            <h5 class="text-center" style="margin-top: 32px">Realtime search engine with Laravel and Pusher</h5>
            <br><br>
            <searchbar></searchbar>
            <products></products>
        </div>

    </div>

    <script async src="{{mix('js/app.js')}}"></script>

    </body>

    </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 😎

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 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