How to build Laravel packages?

How to create a live chat app using Laravel and Vue.js

How to create a live chat app using Laravel and Vue.js

In this tutorial, we will be looking at how to build a live chat using Laravel and Vue.js where people can log in and chat with each other. This is a feature we see in applications like YouTube or Facebook during a live event.

Introduction

In this article, we’ll be building an app where users can log in with their usernames and then join a live chat. Here is what our app will look like:

Requirements

To follow along with this article, you need the following:

If you have these requirements we can begin.

Creating our Laravel application

We will start by creating a new Laravel application. By now, you should have the Laravel CLI, so you can just run this command to create a new project:

$ laravel new larachat

This will create a new Laravel application in a larachat folder.

Setting up Pusher Chatkit in the app

Before we go ahead to enable Chatkit in our app, open the Chatkit app, go to the Console tab and create a new user named admin. Create a new room named live-chat with the admin.

Open the room you have just created and note the room ID. Copy it out as you will soon need it.

To enable Chatkit in our app, we will need to add the keys to our app environment. Add these new properties at the bottom of the .env file found in your project directory:

If you don’t have the .env file for some reason, rename the .env.example file to .env and run the command: php artisan key:generate

    # File: .env
    # [...]

    CHATKIT_INSTANCE_LOCATOR="INSTANCE_LOCATOR_HERE"
    CHATKIT_SECRET_KEY="SECRET_KEY_HERE"
    CHATKIT_GENERAL_ROOM_ID="GENERAL_ROOM_ID"

    MIX_APP_URL="${APP_URL}"
    MIX_CHATKIT_INSTANCE_LOCATOR="${CHATKIT_INSTANCE_LOCATOR}"

You need to replace the placeholder values with the credentials from your Chatkit app. The CHATKIT_INSTANCE_LOCATOR and CHATKIT_SECRET_KEY can be gotten from the Credentials tab of the Chatkit instance. Replace CHATKIT_GENERAL_ROOM_ID with the room ID you copied out earlier. MIX_APP_URL and MIX_CHATKIT_INSTANCE_LOCATOR references the APP_URL and CHATKIT_INSTANCE_LOCATOR respectively.

Still in the .env file, update the APP_URL like so:

    APP_URL=http://localhost:8000

To enable our application can use the environment credentials we added earlier, open the config/services.php file and in there add the snippet below to the array of third-party services:

    // File: config/services.php
    'chatkit' => [
        'secret' => env('CHATKIT_SECRET_KEY'),
        'locator' => env('CHATKIT_INSTANCE_LOCATOR'),
    ],

Next we will need to do is install the Chatkit PHP SDK. Run this command in the root directory of your project to install the Chatkit package:

$ composer require pusher/pusher-chatkit-server

Next, open app/providers/AppServiceProvider.php and add the following code inside the register method:

    // File: app/providers/AppServiceProvider.php
    $this->app->bind('ChatKit', function() {
        return new \Chatkit\Chatkit([
            'instance_locator' => config('services.chatkit.locator'),
            'key' => config('services.chatkit.secret'),
        ]);
    });

The above snippet will bind the Chatkit service within the register method into Laravel’s IoC container. We can now resolve it from anywhere within our app and it will return an instance of the configured Chatkit class.

Building the application logic

Now we have our Laravel application configured and Chatkit SDK setup, let’s write the core functionality of our chat app.

First, we will create and update some controllers. Still in your root directory, run this command to create a new controller:

$ php artisan make:controller ChatkitController

Open the app/Http/Controllers/ChatkitController.php file and replace the contents with this snippet:

    <?php
    // File: app/Http/Controllers/ChatkitController.php

    namespace App\Http\Controllers;

    use Illuminate\Http\Request;

    class ChatkitController extends Controller
    {
        private $chatkit;
        private $roomId;

        public function __construct()
        {
            $this->chatkit = app('ChatKit');
            $this->roomId = env('CHATKIT_GENERAL_ROOM_ID');
        }

        /**
         * Show the welcome page.
         *
         * @return \Illuminate\Contracts\Support\Renderable
         */
        public function index(Request $request)
        {
            $userId = $request->session()->get('chatkit_id')[0];

            if (!is_null($userId)) {
                // Redirect user to Chat Page
                return redirect(route('chat'));
            }

            return view('welcome');
        }

        /**
         * The user joins chat room.
         *
         * @param  \Illuminate\Http\Request $request
         * @return mixed
         */
        public function join(Request $request)
        {
            $chatkit_id = strtolower(str_random(5));

            // Create User account on Chatkit
            $this->chatkit->createUser([
                'id' =>  $chatkit_id,
                'name' => $request->username,
            ]);

            $this->chatkit->addUsersToRoom([
                'room_id' => $this->roomId,
                'user_ids' => [$chatkit_id],
            ]);

            // Add User details to session
            $request->session()->push('chatkit_id', $chatkit_id);

            // Redirect user to Chat Page
            return redirect(route('chat'));
        }

        /**
         * Show the application chat room.
         *
         * @return \Illuminate\Contracts\Support\Renderable
         */
        public function chat(Request $request)
        {
            $roomId = $this->roomId;

            $userId = $request->session()->get('chatkit_id')[0];

            if (is_null($userId)) {
                $request->session()->flash('status', 'Join to access chat room!');
                return redirect(url('/'));
            }

            // Get messages via Chatkit
            $fetchMessages = $this->chatkit->getRoomMessages([
                'room_id' => $roomId,
                'direction' => 'newer',
                'limit' => 100
            ]);

            $messages = collect($fetchMessages['body'])->map(function ($message) {
                return [
                    'id' => $message['id'],
                    'senderId' => $message['user_id'],
                    'text' => $message['text'],
                    'timestamp' => $message['created_at']
                ];
            });

            return view('chat')->with(compact('messages', 'roomId', 'userId'));
        }

        /**
         * Receives a client request and provides a new token.
         *
         * @param  \Illuminate\Http\Request  $request
         * @return mixed
         */
        public function authenticate(Request $request)
        {
            $response = $this->chatkit->authenticate([
                'user_id' => $request->user_id,
            ]);

            return response()
                ->json(
                    $response['body'],
                    $response['status']
                );
        }

         /**
         * Send user message.
         *
         * @param  \Illuminate\Http\Request  $request
         * @return mixed
         */
        public function sendMessage(Request $request)
        {
            $message = $this->chatkit->sendSimpleMessage([
                'sender_id' => $request->user,
                'room_id' => $this->roomId,
                'text' => $request->message
            ]);

            return response($message);
        }

        /**
         * Get all users.
         *
         * @param  \Illuminate\Http\Request  $request
         * @return mixed
         */
        public function getUsers()
        {
            $users = $this->chatkit->getUsers();

            return response($users);
        }

        /**
         * Get all users.
         *
         * @param  \Illuminate\Http\Request  $request
         * @return mixed
         */
        public function logout(Request $request)
        {
            $request->session()->flush();

            return redirect(url('/'));
        }
    }

In the ChatkitController above we have several methods, namely: index, join, chat, authenticate, sendMessage, getUsers and logout.

The index method returns the welcome page if a user ID doesn’t exist in the current session else it redirects to the chat page.

The join method uses the resolved Chatkit SDK instance, creates a new user on Chatkit and add the user to the live-chat room and then save the users Chatkit ID in the current session before redirecting the user to the chat page.

The chat method handles the chat page when loaded on the browser. Inside it, we get the current user Chatkit ID from the session. If a Chatkit ID is not found, we return the user to the welcome page. The Chatkit ID, room ID, and chat room messages properties are then passed to the chat page view.

The authenticate method will act as a token provider server that receives the client's request and returns a valid JWT to your Chatkit client.

In the sendMessage method, we use the SDK’s sendSimpleMessage method, which accepts the sender_id, room_id and text to send a message to the chat room. The getUsers method returns all the users created on our Chatkit instance. Our logout method flushes the current session and redirects to the welcome page.

Next, we will update the web routes. Open routes/web.php and paste the code below to override the default :

    // File: routes/web.php
    Route::get('/', '[email protected]');
    Route::post('/', '[email protected]');
    Route::get('chat', '[email protected]')->name('chat');
    Route::post('logout', '[email protected]')->name('logout');

Open routes/api.php and add the code below in the file:

    // File: routes/api.php
    Route::post('authenticate','[email protected]');
    Route::get('users', '[email protected]');
    Route::post('message','[email protected]');

The newly added routes will provide endpoints to authenticate a client request, show a chat and send a message.

Building the application frontend

First, we will update the welcome page of the app. Open a resources/views/welcome.blade.php file and paste the code below to update the file:

    
    
    getLocale()) }}">
    
        
        
        Live Chat
        
        
        
        
        
        
        
    
    
        
            
                
                    Join Live Chat
                
                @if(Session::has('status'))
                    
                        {{ Session::get('status') }}
                    
                @endif
                
                    
                        @csrf
                        
                            <input type="text" name="username" class="form-control" 
                              placeholder="Enter your username">
                            
                                
                                  Join
                                
                            
                        
                    
                
            
        
    
    

The welcome.blade.php view contains a Join Live Chat title and a simple form that submits the entered username in the input field to join the live chat room.

Next, we will build the chat page to read and send messages. A chatbox component will be created using Vue.js to handle the chat feature.

Open a terminal tab in your project directory and run the command below:

$ npm install

This command installs the required development dependencies in the package.json file. Next, we need to install Chatkit. Run this command to install it:

$ npm install moment @pusher/chatkit-client --save-dev

Now create a ChatBoxComponent.vue file in the resources/js/components/ directory and paste the code snippet below inside the file.

    
    
      
        
            
                * ({{ formatTime(message.timestamp) }}) * : **{{ findSender(message.senderId).name }}**
                {{ message.text }}
            
        
        
* * *

        
            
            
              Send
            
        
      
    

    
    

The ChatBoxComponent.vue single file component is divided into three sections:

  • template tag contains our HTML syntax for the chat box providing the information to be shown.
  • script tag holds the logic that keeps data and methods. It also performs various operations like authentication, sending and receiving messages.
  • style tag provides the simplest option to add CSS to a Vue.js component.

The script tag keeps the logic of our Vue component. Inside this tag, first, we imported the libraries we will make use of. The export default block exports an object literal as our component’s view model and is responsible for the behavior of our component. This block houses other sections.

The props section helps us to pass custom data to the component from its instances. The data section is used to set up the component state. Every property you define in data becomes reactive meaning that if there is a change, it will be reflected in the view.

The method section holds the methods just as the name implies. In our component, we have six methods for various actions.

  • connectToChatkit method handles the connection to Chatkit from the client (browser) interface. It defines a TokenProvider and then the ChatManager is created with the token provider, allowing the client to connect to the Chatkit servers. On a successful connection, the currentUser data state is updated and the subscribeToRoom method is called.
  • subscribeToRoom method handles the connection to a particular room and is able to hook in some actions to respond to certain events like when a new message is sent, a user joins the room or leaves the room.
  • getUser method fetches all users on the Chatkit instance and updates the users data state with the response body gotten.
  • sendMessage method gets data from the input field and makes a post request to send the message.
  • findSender method accepts a senderId parameter and uses this to find a user on the this.users data.
  • formatTime method using the imported moment library returns a human readable time.

The created section is one of the used life cycle hooks used for initialization. In our case, we called the connectToChatkit and getUsers method there.

Next, open resources/js/app.js and add the Vue instance like so:

    // resources/js/app.js
    Vue.component('chatbox', require('./components/ChatBoxComponent.vue').default); 

The above code line registers the ChatBoxComponent.vue using chatbox as its basename. Now, go back to the terminal and run the command below:

$ npm run dev

This command will run a development build process using webpack to compile all resources/js files into the public/js/app.js file.

The command npm run prod is used in the case you want to it in a production environment

Next, create a resources/views/chat.blade.php file and paste the code below:

    
    
    getLocale()) }}">
    
        
        
        
        
        Live Chat
        
        
        
        
        
        
        
    
    
        
            
                <a class="dropdown-item" href="{{ route('logout') }}" onclick="event.preventDefault();
                document.getElementById('logout-form').submit();">
                    Leave Chat Room
                
                
                    @csrf
                
            
            
                
                    
                        
                             Live Chat Room 

                             
                        
                    
                
            
        
    
    

Inside the chat.blade.php, we added the chatbox tag that renders our ChatBoxComponent we created earlier and above it, we have Leave Chat Room link that logs the user out.

Finally, to test our app run the command below to serve the application:

$ php artisan serve

Now open http://localhost:8000 on two separate browser windows. Register two different accounts and try chatting between the two users. You should have results similar to this:

Conclusion

In this tutorial, we have seen how to add a live chatting experience to our app. The application was created using Laravel and Vue.js. The Chatkit API is very extensible and provides features not covered in this tutorial. You can leverage this knowledge to work with other features Chatkit provides in a Laravel app.

The source code is available on GitHub.

Creating a To-Do List App with Vue.js & Laravel

Creating a To-Do List App with Vue.js & Laravel

In this tutorial, we will build a to-do app with Vue.js and Laravel using Pusher to add the realtime functionality. Vue.js is such a great JS framework as it speeds up frontend development and helps achieve more with less code.

Introduction

In this tutorial, we will build a to-do app with Vue.js and Laravel using Pusher to add the realtime functionality. Vue.js is such a great JS framework as it speeds up frontend development and helps achieve more with less code. Laravel on its side is one the most popular and powerful backend frameworks; allied with Vue.js we can build awesome web apps. Pusher you may know is a collection of hosted APIs to build realtime apps with less code. Now let’s get started!

Demo

This is the result of our final working app:

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.

Initialize the project and install dependencies

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

laravel new realtime_todo

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

cd realtime_todo

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 the Laravel CLI again for that. Run this command:

php artisan make:model Task -mc

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

Open your Task.php file and paste this:

 //realtime_todo/app/Task.php
&lt;?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
    //
    protected $fillable = ['title','completed'];
}

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

 //realtime_todo/database/migrations/*_create_tasks_table.php

&lt;?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTasksTable extends Migration
{
    /**
    * Run the migrations.
    *
    * @return void
    */

    public function up() {
        Schema::create('tasks', function (Blueprint $table) {
            $table-&gt;increments('id');
            $table-&gt;string('title');
            $table-&gt;boolean('completed')-&gt;default(false);
            $table-&gt;timestamps();
        }
        );
    }

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

Then run php artisan migrate to run the migration.

Define routes and create the TaskController

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

This is a simple CRUD(create, read, update, delete) over our Task model. So we defined our routes with corresponding functions to handle our browser requests. Paste the following into api.php:

//realtime_todo/routes/api.php
<?php
use Illuminate\Http\Request;

/*
--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::get('todos','[email protected]');
Route::post('todos','[email protected]');
Route::delete('todos/{id}','[email protected]');

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 TaskController file and paste the following code:

 //realtime_todo/app/Http/Controllers/TaskController.php

&lt;?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Events\TaskCreated;
use App\Events\TaskRemoved;
use App\Task;
class TaskController extends Controller
{
    //
    public function fetchAll(){
        $tasks = Task::all();
        //return response()-&gt;json($tasks);
        return $tasks;
    }

    public function store(Request $request){
        $task = Task::create($request-&gt;all());
        broadcast(new TaskCreated($task));
        return response()-&gt;json("added");
    }

    public function delete($id){
        $task = Task::find($id);
        broadcast(new TaskRemoved($task));
        Task::destroy($id);
        return response()-&gt;json("deleted");
    }
}

In the above code we have three functions fetchAllstore , and delete:

  • fetchAll: queries our database to return all our tasks/to-dos
  • store: creates a new to-do with request params(title and task status)
  • delete: finds a task and deletes from the database.

Emit events

Well you may have noticed these lines: broadcast(new Taskcreated($task)) and broadcast(new TaskRemoved($task)) respectively in store and delete functions. What is their purpose? Through these lines we emit events with a task model instance.

You can get more relevant information about Laravel broadcasting here. In the next part of the tutorial, we’ll see how to create these events..

Create our events

In this part we’ll create events we talked about above: TaskCreated and TaskRemoved events.

TaskCreated event

Our TaskCreated event will be emitted whenever a new to-do or task is created. Enough talk, let’s focus on the code. Let’s create our TaskCreated by running the following command in your terminal: php artisan make:event TaskCreated.

Now open your TaskCreated file and paste the following:

 //realtime_todo/app/Events/TaskCreated.php
<?php

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


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

    /**
    * Create a new event instance.
    *
    * @param $task
    * @return void
    */

    public $task;
    public function __construct($task)
    {
        //
        $this-&gt;task = $task;
    }

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

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

    public function broadcastAs(){
        return 'task-created';
    }

}

Our class constructor initializes a task that is created. 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 task-created (which we’ll set up further on the tutorial) on which our event should be broadcast.

TaskRemoved event

This event is broadcast when a task is deleted and we want to notify other users of that.

As you may guess, you can run: php artisan make:event TaskRemoved to create the event. Now head up to your TaskRemoved file:

 //realtime_todo/app/Events/TaskRemoved.php

&lt;?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class TaskRemoved implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
    * Create a new event instance.
    *
    *  @param $task
    * @return void
    *
    */
        public $task;
        public function __construct($task)
        {
        //
        $this-&gt;task = $task;
    }

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

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

    public function broadcastAs(){
        return 'task-removed';
    }

}

This class structure is pretty similar to the previous one, so we won't spend further time explaining its functions.

Don’t forget to implement ShouldBroadcastNow to enable Pusher broadcasting events as soon as they occur.

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

You may also set the encrypted value to false in order to avoid a 500 error while broadcasting events with Pusher.

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 events should be broadcast on particular channels. It’s time to set them up. Paste the following code in your channels.php file:

 //realtime_todo/routes/channels.php
<?php

/*
------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/
Broadcast::channel('newTask', function(){
    return true;
});
Broadcast::channel('taskRemoved', 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
});

While uncommenting, you may also set the encrypted property to false to avoid a 500 error while trying to broadcast events with Pusher channels.

The above code sets up Laravel Echo with Pusher. This will make our app aware of events broadcast, 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 = {
todos: [],
toRemove: null,
newTodo: {
title: '',
completed: false
}
}
export default state

Our state objects holds :

  • todos: holds our to-dos got from the backend
  • toRemove: holds temporarily the to-do we intend to remove
  • newTodo: holds details about a new to-do we are about to add

Create our getters

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

 let getters = {
newTodo: state => {
return state.newTodo
},
todos: state => {
return state.todos
},
toRemove: state => {
return state.toRemove
}
}
export default getters

Create our mutations

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

let mutations = {
ADD_TODO(state, todo) {
state.todos.unshift(todo)
},
CACHE_REMOVED(state, todo) {
state.toRemove = todo;
},
GET_TODOS(state, todos) {
state.todos = todos
},
DELETE_TODO(state, todo) {
state.todos.splice(state.todos.indexOf(todo), 1)
state.toRemove = null;
}
}
export default mutations

Here we have three mutations:

  • ADD_TODO: adds a new to-do to the top our to-dos list
  • CACHE_REMOVED: keeps track temporarily of the to-do to remove
  • GET_TODOS: sets our to-dos list given some data
  • DELETE_TODO: responsible for deleting a to-do from our to-dos list

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 = {
ADD_TODO({commit}, todo) {
axios.post('/api/todos', todo).then(res => {
if (res.data === "added")
console.log('ok')
}).catch(err => {
console.log(err)
})
},
DELETE_TODO({commit}, todo) {
axios.delete(/api/todos/${todo.id})
.then(res => {
if (res.data === 'deleted')
console.log('deleted')
}).catch(err => {
console.log(err)
})
},
GET_TODOS({commit}) {
axios.get('/api/todos')
.then(res => {
{ console.log(res.data)
commit('GET_TODOS', res.data)
}
}).catch(err => {
console.log(err)
})
}
}
export default actions

We have defined two actions and each of them responsible of a single operation. They perform asynchronous calls to our API routes.

  • ADD_TODO makes a POST request to our api/todos endpoint to add a new task. This action is dispatched whenever the user is submitting a task to add.
  • GET_TODOS sends a GET request to our api/todos endpoint to get our database to-dos/tasks and commits the response with GET_TODOS mutation.
  • DELETE_TODO performs a DELETE a request to our api/todos/{id} endpoint in order to remove a to-do from our to-dos list.

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. Replace the existing code in your ../resouces/js/app.js file with the following:

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

Vue.component('todo', require('./components/Todo'))
Vue.component('new-todo', require('./components/NewTodo.vue'))
Vue.component('todo-list', require('./components/TodoList'))
Vue.component('todo-app', require('./components/TodoApp'))
import store from '../js/store'

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

The previous code also globally registers four Vue components, Todo.vue ,NewTodo.vue,TodoList.vueand TodoApp.vue that we’ll build in the next part of this tutorial.

Building Vue components

We’ll build four Vue components for our app as we said above, so let’s do it.

Create the Todo.vue component

The Todo.vue component is responsible for encapsulating details about a single Task instance from the database and rendering it in a proper and styled way. Create a Todo.vue file in your ../resources/js/components directory and paste the following inside:

//../resources/js/components/Todo.vue
<template>
<li class="todo" :class="{ completed: todo.completed }">
<div class="view">
<input type="checkbox" v-model="todo.completed" class="toggle">
<label>{{todo.title}}</label>
<button @click="removeTodo(todo)" class="destroy"></button>
</div>
</li>
</template>
<script>
export default {
name: "Todo",
props: ["todo"],

  methods: {
    removeTodo(todo) {
      this.$store.commit("CACHE_REMOVED", todo)
      this.$store.dispatch("DELETE_TODO", todo);
    }
  }
};
&lt;/script&gt;

Our Todo.vue component takes a todo property whose details we render in the component body using the HTML <li></li> tag. The component has the removeTodo function that takes the to-do we intend to remove as argument, caches it temporarily (via the CACHE_REMOVED mutation) and dispatches the DELETE_TODO action to remove it.

Create the NewTodo.vue component

We’ll use this component to add a new task to our to-dos list. This component should be very simple to code. I promise you . First create the NewTodo.vue file inside the same directory as above and paste this inside:

//../resources/js/components/NewTodo.vue
<template>
<input type="text" v-model="newTodo.title"
@keyup.enter="addTodo" autofocus="autofocus"
placeholder="What are you trying to get done?" class="new-todo">
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "NewTodo",
methods: {
addTodo() {
this.$store.dispatch("ADD_TODO", this.newTodo);
}
},
computed: {
...mapGetters(["newTodo"])
}
};
</script>

This component is composed of a simple input field to enter our to-do title. We append a @keyup.enter event so we can execute the addTodo function whenever the user hits the Enter key of his keyboard. Nice play isn’t it ?! We get the newTodo state object from our getters using Vue.js mapGetters helper and bind it to our input. As I said above the newTodo object should contain information of a new todo we want to add to our to-dos list. The addTodo function dispatches the ADD_TODO action having our newTodo as a parameter.

Create the TodoList.vue component

This component will render to-dos items from database. It’s that simple. So create your TodoList.vuecomponent and paste this code inside:

 //../resources/js/components/TodoList.vue
<template>
<ul class="todo-list">
<todo v-for="todo in todos" :todo="todo" :key="todo.id" />
</ul>
</template>

&lt;script&gt;
import { mapGetters } from "vuex";
import todo from "../components/Todo";
export default {
  components: {
    todo
  },
  name: "TodoList",
  mounted() {
    this.$store.dispatch("GET_TODOS");
  },
  computed: {
    ...mapGetters(["todos"]),
  }
};
&lt;/script&gt;

In the mounted hook function we dispatch the GET_TODOS action to get our to-dos list item, and we use Vuex helper function …mapGetters() to access our todos state. We loop over our to-dos list and render a todo component (imported from Todo.vue component) which takes the current loop item as a property.

Create the TodoApp.vue component

In this component we simply merge the first three components we created and listen to Pusher realtime events. Create your TodoApp.vue component and paste the following inside:

 //../resources/js/components/TodoApp.vue

 &lt;template&gt;
     &lt;section class="todoapp"&gt;
            &lt;header class="header"&gt;
              &lt;h1&gt;todos&lt;/h1&gt;
            &lt;/header&gt;
            &lt;new-todo&gt;&lt;/new-todo&gt;
            &lt;todo-list&gt;&lt;/todo-list&gt;
     &lt;/section&gt;
 &lt;/template&gt;
 &lt;script&gt;
import newTodo from "../components/NewTodo.vue";
import todoList from "../components/TodoList.vue";
import { mapGetters } from "vuex";

export default {
  components: {
   newTodo,
   todoList
  },
  name: "TodoApp",
  mounted() {
    window.Echo.channel("newTask").listen(".task-created", e =&gt; {
      this.$store.commit("ADD_TODO", e.task);
      this.newTodo.title = "";
    });
    window.Echo.channel("taskRemoved").listen(".task-removed", e =&gt; {
        this.$store.commit("DELETE_TODO", this.toRemove);
    });
  },
  computed: {
    ...mapGetters(["newTodo", "toRemove"])
  }
};
&lt;/script&gt;

In the mounted function of our component, we are subscribing to two channels:

  • newTask channel: we listen the task-created event triggered when a new to-do is added to the list. Then we commit the ADD_TODO mutation with the task sent carried by the event, in order to add it to our to-dos list. Finally we reset our newTodo we import from our Vuex store.
  • taskRemoved channel, this channel enables to listen to the task-removed event triggered when a task/to-do is removed from our list. When the event is emit, we assign the task deleted to our toRemove object we set up in our Vuex store, and we commit the DELETE_TODO mutation to finally remove it from to-dos list.
Finalize the app

Now, let’s replace of our welcome.blade.php with the following containing our TodoApp component:

//realtime_todo/resources/views/welcome.blade.php

&lt;!doctype html&gt;
&lt;html lang="{{ str_replace('_', '-', app()-&gt;getLocale()) }}"&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
        &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
        &lt;meta name="csrf-token" content="{{ csrf_token() }}" /&gt;
        &lt;title&gt;Realtime to-do app&lt;/title&gt;
        &lt;!-- Fonts --&gt;
        &lt;link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet" type="text/css"&gt;
        {{--  Style  --}}
        &lt;link href="{{ mix('css/app.css') }}" rel="stylesheet" type="text/css"&gt;
        &lt;!-- Styles --&gt;
        &lt;style&gt;
            html, body {
                background-color: #fff;
                color: #636b6f;
                font-family: 'Nunito', sans-serif;
                font-weight: 200;
                height: 100vh;
                margin: 0;
            }
            .full-height {
                height: 100vh;
            }
            .flex-center {
                align-items: center;
                display: flex;
                justify-content: center;
            }
            .position-ref {
                position: relative;
            }
            .top-right {
                position: absolute;
                right: 10px;
                top: 18px;
            }
            .content {
                text-align: center;
            }
            .title {
                font-size: 84px;
            }
            .links &gt; a {
                color: #636b6f;
                padding: 0 25px;
                font-size: 12px;
                font-weight: 600;
                letter-spacing: .1rem;
                text-decoration: none;
                text-transform: uppercase;
            }
            .m-b-md {
                margin-bottom: 30px;
            }
        &lt;/style&gt;
    &lt;/head&gt;
    &lt;body&gt;
          &lt;div id="app"&gt;
                    &lt;todo-app&gt;&lt;/todo-app&gt;
          &lt;/div&gt;

          &lt;script src="{{mix('js/app.js')}}"&gt;&lt;/script&gt;
    &lt;/body&gt;
&lt;/html&gt;

To style our app, get this file and replace the content inside your ../resources/sass/app.scss file with it.

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. You can try to add a new to-do to your list and see things working in realtime if you carefully followed steps above. You are now a boss

Note: If you encounter a 500 error when trying to add or delete to-dos, it’s sure that you have to disable Pusher encryption as I suggested you. 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 to-do app 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.

Thanks for reading. If you liked this post, share it with all of your programming buddies!

Originally published on pusher.com

How to build Laravel packages with Vue.js

How to build Laravel packages with Vue.js

In this tutorial, we will learn how to build a Laravel package with Vue.js. The sample package we will be building will be a review system that can be added to a page you are building independently.

This package will ensure that the reviews on each page are updated in realtime so that visitors don’t have to refresh the page to see new reviews.

When we are done, our app will look like this:

Prerequisites

To follow along with this article, you need the following:

Let’s get started.

Setup the Laravel project

The first thing we need to do is create a Laravel project. We will use the Laravel installer to make this possible. Open your terminal, and run this command:

    $ laravel new sampleLaravelApp

This will create a Laravel application named sampleLaravelApp. Next, we will install a Composer package called laravel-packager. We need this to build our Laravel package. In the root directory of the Laravel project run this command:

    $ composer require jeroen-g/laravel-packager --dev

Now we have installed the package. We can use it to scaffold our own package. Run this command in your projects directory:

    $ php artisan packager:new Acme PageReview --i

The package will be created interactively. It will interactively ask some questions and your responses will be used to create a composer.json file for our package.

Now you should have a folder named packages inside your Laravel project. Inside the packages folder, you will find a directory with your vendor name (Acme in our case), a subfolder representing the package name (PageReview).

Building the Laravel package

In this section, we will start customizing the generated package files with the actual code and add some new files where necessary. Before adding logic, note that when we were setting up the new Laravel package, laravel-packager enabled Package Discovery for us.

This is a Laravel feature that automatically loads a package’s service provider. In your package’s composer.json file, this is the snippet that enables it:

    // File: ./packages/Acme/PageReview/composer.json
    "extra": {
      "laravel": {
        "providers": [
          "Acme\\PageReview\\PageReviewServiceProvider"
        ],
        "aliases": {
          "PageReview": "Acme\\review\\Facades\\PageReview"
        }
      }
    }

Adding our database migration file

Next, let’s create a migration file to help us save reviews to the database. Create a new folder named database in the packages/Acme/PageReview directory. Inside this folder, create a migrations folder.

Run this command in the Laravel project root directory to create the package migration:

    $ php artisan make:migration create_page_reviews_tables --path=packages/Acme/PageReview/database/migrations

Update the up() and down()method of the migration file as seen below:

    // File: packages/Acme/PageReview/database/migrations/*_create_page_reviews_table.php

    public function up()
    {
        Schema::create('pages', function (Blueprint $table) {
            $table->increments('id');
            $table->text('path');
            $table->timestamps();
        });

        Schema::create('reviews', function (Blueprint $table) {
            $table->increments('id');
            $table->text('page_id');
            $table->text('username');
            $table->text('comment');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('pages');
        Schema::dropIfExists('reviews');
    }

It is not recommended to create one migration for two tables but for brevity we will. However, in a real application, create one migration per database operation.

In the up method, we have create two tables, the pages and reviews table. The pages table will save the URL path visited in our app. The reviews table will contain the username and comment field for the actual review. We included page_id field because we intend to create a link between the reviews and pages. The down method will drop both tables when triggered.

Adding the package model files

In our packages/Acme/PageReview directory open the src directory and create a new folder called Models. Inside the folder, create two new files called Review.php and Page.php.

Open the Review.php and paste this snippet:

    <?php
    // File: packages/Acme/PageReview/src/Models/Review.php

    namespace Acme\PageReview\Models;

    use Illuminate\Database\Eloquent\Model;

    class Review extends Model
    {
        protected $fillable = [
            'page_id', 
            'username', 
            'comment'
        ];
    }

Next, open the Page.php and paste this snippet:

    <?php
    // File: packages/Acme/PageReview/src/Models/Page.php

    namespace Acme\PageReview\Models;

    use Illuminate\Database\Eloquent\Model;

    class Page extends Model
    {
        protected $fillable = [
          'path',
        ];

        public function reviews()
        {
            return $this->hasMany(Review::class);
        }
    }

In the code above, the Page model has a reviews method, which forms a hasMany relationship with the Review model. This simply means a page can have many reviews.

Updating the package config file

We already have a config folder inside our package directory. Inside this config folder there is a single file representing our package configurations, where anyone using this package can easily override the default configuration options. The name of the file is pagereview.php.

Update the pagereview.php file like so:

    <?php
    // File: packages/Acme/PageReview/config/pagereview.php

    return [
        /*
        * This determines how the reviews will be ordered when fetched
        */
        'order' => [
            'by' => 'DESC',
            'as' => 'created_at',
        ],
    ];

Setting the package controller methods for certain actions

Let’s create a new folder Controllers inside the packages/Acme/PageReview/src directory. Inside the folder, create a file called PageReviewController.php and paste this snippet there:

    <?php
    // File: packages/Acme/PageReview/src/Controllers/PageReviewController.php

    namespace Acme\PageReview\Controllers;

    use Illuminate\Http\Request;
    use Acme\PageReview\Models\Page;
    use Illuminate\Routing\Controller;
    use Pusher\Laravel\Facades\Pusher;

    class PageReviewController extends Controller 
    {
        public function index(Request $request)
        {
            if (isset($request->path)) {
              $page = Page::firstorCreate(['path' => $request->path]);

              $reviews = $page->reviews()
                      ->orderBy(
                          config('pagereview.order.as'),
                          config('pagereview.order.by')
                      )
                      ->get();

              return response()->json([
                  'page' => $page,
                  'reviews' => $reviews
              ]);
            }

            return response()->json([]);
        }

        public function store(Request $request)
        {
            $page = Page::firstorCreate(['path' => $request->path]);

            $review = $page->reviews()->create([
              'username' => $request->username,
              'comment' => $request->comment,
            ]);

            Pusher::trigger('page-'.$page->id, 'new-review', $review);

            return $review;
        }
    }

This controller file has two methods:

  • index: here we get a path and save it if it is not already saved, then we get the related reviews to that page and return both the page and reviews as a JSON response.
  • store: here we simply save the page, and review. We also trigger a message to the Pusher API so that channels listening can pick up the review.

Next, let's add the Pusher package as a dependency. Open the composer.json in the package and add it like so:

    // File: packages/Acme/PageReview/composer.json
    "require": {
        // ...
        "pusher/pusher-http-laravel": "^4.2"
    },

Defining routes

Next, we will create a routes folder in the packages/Acme/PageReview directory to save our routes. After creating the folder, create a web.php file inside the folder and update it with the snippet below:

    <?php
    // File: packages/Acme/PageReview/routes/web.php
    Route::get('pagereview', '[email protected]')->name('pagereview.index');
    Route::post('pagereview', '[email protected]')->name('pagereview.store');

Updating the service provider

Most Laravel packages come with migrations, routes, config files or views. To be able to use these resources we have to load them in our package’s service provider.

You can read more about that in the official documentation

Update the PageReviewServiceProvider file like so:

    <?php
    // packages/Acme/PageReview/src/PageReviewServiceProvider.php

    namespace Acme\PageReview;

    use Illuminate\Support\ServiceProvider;

    class PageReviewServiceProvider extends ServiceProvider
    {
        /**
         * Perform post-registration booting of services.
         *
         * @return void
         */
        public function boot()
        {
            $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
            $this->loadViewsFrom(__DIR__.'/../resources/views', 'pagereview');

            $this->app['router']->namespace('Acme\\PageReview\\Controllers')
                ->middleware(['web'])
                ->group(function () {
                    $this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
                });

            if ($this->app->runningInConsole()) {
                $this->bootForConsole();
            }
        }

        /**
         * Register any package services.
         *
         * @return void
         */
        public function register()
        {
            $this->mergeConfigFrom(__DIR__.'/../config/pagereview.php', 'pagereview');
        }

        /**
         * Get the services provided by the provider.
         *
         * @return array
         */
        public function provides()
        {
            return ['pagereview'];
        }

        /**
         * Console-specific booting.
         *
         * @return void
         */
        protected function bootForConsole()
        {
            $this->publishes([
                __DIR__.'/../config/pagereview.php' => config_path('pagereview.php'),
            ], 'pagereview.config');

            $this->publishes([
                __DIR__.'/../resources/views' => base_path('resources/views/vendor/acme'),
            ], 'pagereview.views');

            $this->publishes([
                __DIR__ . '/../database/migrations/' => database_path('migrations'),
            ], 'migrations');
        }
    }

The boot method loads resources such as migration, routes, or any other piece of functionality while the register method only binds logic to the service container. We also have a bootForConsole method meant to publish resource files such as views and migrations if it is loaded via the console.

Building the frontend

Now, we will create the frontend view for the package’s review section. It will be a simple UI that displays a form to add a review and also display a list of reviews with the author name.

Create a resources/views folder in the packages/Acme/PageReview directory and create a file called section.blade.php. Paste this snippet to the file:

    <!-- File: packages/Acme/PageReview/resources/views/section.blade.php -->
    <div class="container" id="review">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-body">
                        <h4>Add review</h4>
                        <div class="form-group">
                            <input type="text" v-model="username" class="form-control col-4" placeholder="Enter your username">
                        </div>
                        <div class="form-group">
                            <textarea class="form-control" v-model="comment" placeholder="Enter your review"></textarea>
                        </div>
                        <div class="form-group">
                            <input type="button" v-on:click="addPageReview" v-bind:disabled="isDisabled" class="btn btn-success" value="Add Review" />
                        </div>
                        <hr />
                        <h4>Display reviews</h4>
                        <div v-for="review in reviews">
                            <strong>@{{ review.username }}</strong>
                            <p>@{{ review.comment }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

In the code above we added a few attributes to some elements:

  • Both input fields have a v-model Vue directive allowing us to create a two-way binding.
  • The Add Review button has a v-on:click to intercept any DOM event by and triggers the addPageReview method on the Vue instance.
  • The v-for attribute enables us to iterate over the reviews data and display each review belonging to that page.

Still in the same file, add the following below the closing div tag:

    <!-- File: packages/vendor-name/PageReview/resources/views/section.blade.php -->
    <!-- Add Vue Code -->
    <script>
        Pusher.logToConsole = true;
        var review = new Vue({
            el: '#review',
            data: {
                username: null,
                comment: null,
                path: window.location.pathname,
                isDisabled: false,
                reviews: [],
                page: [],
            },
            methods: {
                subscribe() {
                    var pusher = new Pusher('{{ env('PUSHER_APP_KEY')}}', {
                        cluster: '{{ env('PUSHER_APP_CLUSTER') }}',
                    });
                    pusher.subscribe('page-' + this.page.id)
                        .bind('new-review', this.fetchPageReviews);
                },
                fetchPageReviews() {
                    var vm = this;
                    var url = '{{ route('pagereview.index') }}' + '?path=' + this.path;

                    fetch(url)
                      .then(function(response) {
                          return response.json()
                      })
                      .then(function(json) {
                          vm.page = json.page
                          vm.reviews = json.reviews
                          vm.subscribe();
                      })
                },
                addPageReview(event) {
                    event.preventDefault();
                    this.isDisabled = true;
                    const token = document.head.querySelector('meta[name="csrf-token"]');
                    const data = {
                        path: this.path,
                        comment: this.comment,
                        username: this.username,
                    };
                    fetch('{{ route('pagereview.store') }}', {
                        body: JSON.stringify(data),
                        credentials: 'same-origin',
                        headers: {
                            'content-type': 'application/json',
                            'x-csrf-token': token.content,
                        },
                        method: 'POST',
                        mode: 'cors',
                    }).then(response => {
                        this.isDisabled = false;
                        if (response.ok) {
                            this.username = '';
                            this.comment = '';
                            this.fetchPageReviews();
                        }
                    })
                },            
            },
            created() {
                this.fetchPageReviews();
            }
        });
    </script>

Here, we have methods to fetch reviews and add reviews on a page. Our Vue instance is instantiated on the review ID and has some data values.

We also defined three methods:

  • subscribe - this subscribes to a new-review event on a Pusher channel on the current page for realtime update.
  • fetchPageReviews - this method will fetch all the existing reviews for this page when the loaded and also calls the subscribe method.
  • addPageReview - this will collect the input field values of username and text to as a review, thereby triggering an event broadcast on the backend, so all clients will receive in real time.

Finalizing and testing the package

Here, we will test the package we have just built. Inside the main Laravel app composer.json file we need to add a new repository definition like this:

    // [...]

    "repositories": [
      {
        "type": "path",
        "url": "./packages/*/*/",
        "options": {
            "symlink": true
        }
      }
    ]

    // [...]

This will let Composer include a new repository that contains our package and symlink it to the package code folder. Now to use our package in the Laravel app, add this snippet to the require section in composer.json file:

    // [...]

    "require": {
        "Acme/pagereview": "@dev"
    }

    // [...]

After that, run this command in the Laravel app directory:

    $ composer update --prefer-source 

This command will update all packages required and also bring in our newly developed local package.

Now, we need to add a layout file using the auth artisan command. Inside our Laravel app directory, run the command below:

    $ php artisan make:auth

In the root directory of our Laravel project open the resources/views/layouts folder and open a file called app.blade.php. After that, add this code below the last div tag:

    <script src="{{ asset('js/app.js') }}"></script>
    <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
    @include('pagereview::section')
  • The script tag is used to fetch the Pusher.js library file.
  • @include is a Laravel blade directive that allows you to include a blade view from within another view. In this case, we are including our package section.blade.php view.

Now, create a new pagereview.blade.php file inside the resources/views folder of the Laravel app directory. After creating it, paste the snippet below:

    @extends('layouts.app')
    @section('content')
    <div class="container">
        <div class="d-flex justify-content-center">
            <div>
                <h1 class="mb-4">
                    Laravel Page Review
                </h1>
                <p>A simple package to add reviews to a page</p>
            </div>
        </div>
    </div>
    @endsection

Also, open routes/web.php in the Laravel app and add this snippet:

    Route::get('/test', function () {
        return view('pagereview');
    });

Now, open the .env file in our Laravel app directory and update the following properties with keys from your Pusher dashboard:

    PUSHER_APP_ID="PUSHER_APP_ID"
    PUSHER_APP_KEY="PUSHER_APP_KEY"
    PUSHER_APP_SECRET="PUSHER_APP_SECRET"
    PUSHER_APP_CLUSTER="PUSHER_APP_CLUSTER"

We will use SQLite for testing our package. So, still in the same .env file, update the .env configurations below:

    DB_CONNECTION=sqlite
    DB_DATABASE=/path/to/database.sqlite

Next, create a new SQLite database by creating a file called database.sqlite in the database directory.

Next, run this command to migrate the database:

    $ php artisan migrate

Finally, let’s serve the application using this command:

    $ php artisan serve

To test that our Laravel package is working properly visit the page URL http://localhost:8000/test on two separate browser windows. Then make a review on the same page on each of the browser windows and check that it updates in realtime on the other window.

Conclusion

So far, we learned how to build a simple Laravel package using Laravel and Vue. In this tutorial, we created a reviews package for pages and also made it realtime using Pusher. which can be used in any Laravel application.

The code is available on GitHub.