Laravel, PHP

Ứng dụng trò chuyện Laravel Ratchet Websocket thời gian thực

Trong hướng dẫn này, chúng ta sẽ thảo luận về Cách tạo Ứng dụng Trò chuyện Thời gian Thực trong Laravel 8 Framework bằng cách sử dụng Thư viện Web Ratchet. Trong hướng dẫn này, chúng tôi sẽ xây dựng Ứng dụng trò chuyện thời gian thực, trong đó người dùng của chúng tôi có thể thực hiện cuộc trò chuyện thời gian thực thông qua Ứng dụng trò chuyện này. 

Bước 1: Cài một ứng dụng laravel

Đầu tiên hãy chạy câu lệnh để cài dự án (như mình dùng laravel 8 và cài bằng composer)

composer create-project laravel/laravel:^8.0 chat-realtime

Bạn có thể tìm hiểu thêm ở trang này.

Bước 2 : Tạo kết nối cơ sở dữ liệu

Chúng ta hãy mở tệp .env để kết nối với cơ sở dữ liệu, như dự án của mình thì mình cấu hình như dưới đây.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=chat-group
DB_USERNAME=root
DB_PASSWORD=

Bây giờ mình muốn thêm bốn cột bảng trong bảng người dùng, vì vậy đối với điều này, trong dấu nhắc lệnh, chúng ta phải làm theo lệnh.

php artisan make:migration update_users_table --table=users

Sau đó bạn vào file mình vừa tạo sửa như sau như của mình có đường dẫn là database\migrations\2023_05_11_081925_update_users_to_users_table.php

<?php

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

class UpdateUsersToUsersTable extends Migration

{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {

            $table->string('token')->nullable();
            $table->integer('connection_id')->nullable();
            $table->enum('user_status', ['Offline', 'Online']);
            $table->string('user_image')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('token');
            $table->dropColumn('connection_id');
            $table->dropColumn('user_status');
            $table->dropColumn('user_image');
        });
    }
}

Chúng ta có token để lưu chuỗi có 20 ký tự bất kỳ dùng để thay id, connection_id đại diện cho client mỗi khi đăng nhập sẽ phát sinh ra một số ngẫu nhiên đại diện cho client đó khi vào trang chúng ta update trường này để so sánh rồi gửi dữ liệu từ backend ra client, user_status dùng để cập nhật trạng thái Online hay Offline của user (hiện tại mình chưa chú trọng chức năng này ), user_image là ảnh của user.

Sau đó chúng ta sửa file Model như sau:

app\Models\User.php

<?php

namespace App\Models;


use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable

{

    use HasFactory;
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'connection_id', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [

        'password', 'remember_token',

    ];


    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [

        'email_verified_at' => 'datetime',

    ];

}

Tiếp theo chúng ta tạo Model với migrate chat để lưu nội dung chat với câu lệnh sau:

php artisan make:model Chat -m

Sau đó bạn mở file vừa tạo thêm đoạn mã sau:

database\migrations\2023_05_11_073800_create_chats_table.php

<?php

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

class CreateChatsTable extends Migration

{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('chats', function (Blueprint $table) {
            $table->id();
            $table->integer('from_user_id');
            $table->integer('to_group_id');
            $table->string('chat_message');
            $table->string('message_status');
            $table->timestamps();
        });
    }

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

Tiếp theo mình tạo một Model và migrate Group để lưu những nhóm chat (mục đích mình muốn phân biệt đâu là chat riêng đâu là chat nhóm) các bạn chạy câu lệnh sau :

php artisan make:model Group -m

Sau đó vào file vừa tạo ở database sửa đoạn mã thành

database\migrations\2023_05_11_080430_create_groups_table.php

<?php

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

class CreateGroupsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('groups', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->enum('group_status', ['Private', 'Groups']);
            $table->timestamps();
        });
    }

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

Model bạn sẽ sửa như sau

app\Models\Group.php

<?php

namespace App\Models;


use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Group extends Model

{

    use HasFactory;
    protected $fillable = [
        'id', 'name', 'group_status', 'created_at', 'updated_at',

    ];

    protected $table = 'groups';


    public function user_of_group()
    {
        return $this->hasOne(UserOfGroup::class, 'group_id', 'id');

    }
}

Tiếp theo bạn chạy câu lệnh:

php artisan make:model UserOfGroup -m

Ở đây mình tạo thêm bảng để phân biệt nhóm user ở group nào, sau đó sửa đoạn code thành như sau tại file mới tạo

<?php

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

class CreateUserOfGroupsTable extends Migration

{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('user_of_groups', function (Blueprint $table) {
            $table->id();
            $table->integer('group_id');
            $table->integer('user_id');
            $table->timestamps();
        });
    }

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

Trong Model chúng ta sẽ sửa như sau:

app\Models\UserOfGroup.php

<?php

namespace App\Models;


use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;


class UserOfGroup extends Model

{

    use HasFactory;
    protected $fillable = [
        'id', 'user_id', 'group_id', 'created_at', 'updated_at',
    ];
    protected $table = 'user_of_groups';

    public function group()
    {
        return $this->hasOne(Group::class, 'id', 'group_id');

    }

    public function users()
    {
        return $this->hasOne(User::class, 'id', 'user_id');
    }
}

Về phần Model và Migrate đã chuẩn bị xong chúng ra chạy câu lệnh để tạo các bảng

php artisan migrate

Bước 3 : Tải xuống và cài đặt thư viện Ratchet WebSocket

Tại ứng dụng của mình mình đã sử dụng thư viện Ratchet các bạn chạy câu lệnh sau

composer require cboden/ratchet

Sau đó mình tạo command mục đích chạy để khởi tạo server

php artisan make:command WebSocketServer --command=websocket:init

Sau khi chạy lệnh trên, nó sẽ tạo tệp Máy chủ Websocket trong app/Console/Commands/WebSocketServer.php trong thư mục này. Bạn hãy chỉnh sửa tệp như dưới đây:

<?php

namespace App\Console\Commands;


use App\Http\Controllers\WebSocketController;
use Illuminate\Console\Command;
use Ratchet\Http\HttpServer;
use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;


class WebSocketServer extends Command

{

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'websocket:init';


    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $server = IoServer::factory(
            new HttpServer(
                new WsServer(
                    new WebSocketController()
                )
            ),
            8090
       );
        $server->run();
    }
}

Bước 4 : Tạo Controller

Dưới đây bạn tạo sử dụng câu lệnh tạo controller để xử lý các task vụ:

php artisan make:controller WebSocketController
php artisan make:controller SampleController

Lệnh này sẽ tạo tệp SampleController.php, WebSocketController.php trong thư mục app/Http/Controllers. Tại tệp SampleController.php chúng ta sẽ sử lý các tính năng đăng ký, đăng nhập, đăng xuất cho ứng dụng này

app\Http\Controllers\SampleController.php

<?php

namespace App\Http\Controllers;


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

class SampleController extends Controller
{
    public function index(Request $request)
    {
        return view('login');
    }

    public function registration()
    {
        return view('registration');
    }

    public function welcome()
    {
        return view('welcome');
    }

    public function validate_registration(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:6',
        ]);

        $data = $request->all();

        User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);

        return redirect('login')->with('success', 'Registration Completed, now you can login');
    }

    public function validate_login(Request $request)
    {
        $request->validate([
            'email' => 'required',
            'password' => 'required',
        ]);

        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            $token = Str::random(20);

            User::where('id', Auth::id())->update(['token' => $token]);

            return redirect('/');
        }

        return redirect('login')->with('success', 'Login details are not valid');
    }

    public function logout()
    {
        Auth::logout();

        return Redirect('login');
    }
}

Tiếp theo là sử lý các chức năng liên quan đến đoạn chat:

app\Http\Controllers\WebSocketController.php

<?php

namespace App\Http\Controllers;

use App\Models\Chat;
use App\Models\Group;
use App\Models\User;
use App\Models\UserOfGroup;
use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;

class WebSocketController extends Controller implements MessageComponentInterface

{
    protected $clients;


    public function __construct()
    {
        $this->clients = new \SplObjectStorage();
    }


    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);
        $querystring = $conn->httpRequest->getUri()->getQuery();
        parse_str($querystring, $queryarray);
        if (isset($queryarray['token'])) {
            User::where('token', $queryarray['token'])->update(['connection_id' => $conn->resourceId, 'user_status' => 'Online']);

            $user_id = User::select('id')->where('token', $queryarray['token'])->get();

            $data['id'] = $user_id[0]->id;

            $data['status'] = 'Online';

            foreach ($this->clients as $client) {
                if ($client->resourceId != $conn->resourceId) {
                    $client->send(json_encode($data));
                }
            }
        }
    }

    public function onMessage(ConnectionInterface $conn, $msg)
    {
        $data = json_decode($msg);
        if (isset($data->type)) {
            if ($data->type == 'request_search_user') {
                $user_data = User::select('id', 'name', 'user_status', 'user_image')
                                    ->where('id', '!=', $data->from_user_id)
                                    ->where('name', 'like', '%'.$data->search_query.'%')
                                    ->orderBy('name', 'ASC')
                                    ->get();

                $sub_data = [];
                foreach ($user_data as $row) {
                    $sub_data[] = [
                        'name' => $row['name'],
                        'id' => $row['id'],
                        'status' => $row['user_status'],
                        'user_image' => $row['user_image'],
                    ];
                }

                $sender_connection_id = User::select('connection_id')->where('id', $data->from_user_id)->get();

                $send_data['data'] = $sub_data;

                $send_data['response_search_user'] = true;

                foreach ($this->clients as $client) {
                    if ($client->resourceId == $sender_connection_id[0]->connection_id) {
                        $client->send(json_encode($send_data));
                    }
                }
            }
            if ($data->type == 'room_chat_user') {
                $group = Group::select('id', 'name', 'group_status')
                    ->with('user_of_group')
                    ->whereHas('user_of_group.users', function ($query) use ($data) {
                        $query->where('id', $data->from_user_id);
                    })->get();
                $sender_connection_id = User::select('connection_id')->where('id', $data->from_user_id)->get();

                $send_data['data'] = $group;

                $send_data['response_group_user'] = true;
                foreach ($this->clients as $client) {
                    if ($client->resourceId == $sender_connection_id[0]->connection_id) {
                        $client->send(json_encode($send_data));
                    }
                }
            }
            if ($data->type == 'history_group_chat') {
                $chat = Chat::select('id', 'from_user_id', 'to_group_id', 'chat_message', 'message_status')
                        ->with('users')
                        ->where('to_group_id', $data->group_chat_id)
                        ->orderBy('id', 'ASC')
                        ->get();
                $send_data['chat_history'] = $chat;
                $send_data['to_group_id'] = $data->group_chat_id;

                $receiver_connection_id = User::select('connection_id')->where('id', $data->from_user_id)->get();

                foreach ($this->clients as $client) {
                    if ($client->resourceId == $receiver_connection_id[0]->connection_id) {
                        $client->send(json_encode($send_data));
                    }
                }
            }
            if ($data->type == 'request_send_message') {
                $chat = Chat::create([
                    'from_user_id' => (int) $data->from_user_id,
                    'to_group_id' => (int) $data->to_group_id,
                    'chat_message' => $data->chat_message,
                    'message_status' => 'Send',
                ]);

                $userOfGroup = UserOfGroup::where('group_id', $data->to_group_id)
                                ->with('users')->get();
                $arrIdGroup = [];
                foreach ($userOfGroup as $value) {
                    array_push($arrIdGroup, $value->users->connection_id);
                }

                foreach ($this->clients as $client) {
                    if (in_array($client->resourceId, $arrIdGroup)) {
                        $new_chat = Chat::select('id', 'from_user_id', 'to_group_id', 'chat_message', 'message_status')
                            ->with('users')
                            ->where('to_group_id', $data->to_group_id)
                            ->where('id', $chat->id)
                            ->first();
                        $send_data['message'] = $new_chat;
                        $client->send(json_encode($send_data));
                    }
                }
            }
            if ($data->type == 'create_private_chat') {
                $group = Group::where(function ($query) use ($data) {
                    $query->where('name', $data->name_from_user_id.','.$data->name_to_user_id)
                    ->orWhere('name', $data->name_to_user_id.','.$data->name_from_user_id);
                })
                ->where('group_status', 'Private')
                ->get();
                if (count($group->toArray()) == 0) {
                    $new_group = Group::create([
                        'name' => $data->name_from_user_id.','.$data->name_to_user_id,
                        'group_status' => 'Private',
                    ]);
                    UserOfGroup::create(['group_id' => $new_group->id, 'user_id' => $data->from_user_id]);
                    $receiverUserOfGroup = UserOfGroup::create(['group_id' => $new_group->id, 'user_id' => $data->to_user_id]);

                    $chat = Chat::select('id', 'from_user_id', 'to_group_id', 'chat_message', 'message_status')
                        ->with('users')
                        ->where('to_group_id', $new_group->id)
                        ->orderBy('id', 'ASC')
                        ->get();
                    $send_data['chat_history'] = $chat;
                    $send_data['to_group_id'] = $new_group->id;

                    $send_connection_id = User::select('connection_id')->where('id', $data->from_user_id)->get();

                    foreach ($this->clients as $client) {
                        if ($client->resourceId == $send_connection_id[0]->connection_id) {
                            $client->send(json_encode($send_data));
                        }
                    }
                    $group = Group::select('id', 'name', 'group_status')
                    ->with('user_of_group')
                    ->whereHas('user_of_group.users', function ($query) use ($data) {
                        $query->where('id', $data->to_user_id);
                    })->get();
                    $receiver_connection_id = User::select('connection_id')->where('id', $data->to_user_id)->get();

                    $receiver_data['data'] = $group;

                    $receiver_data['response_group_user'] = true;
                    foreach ($this->clients as $client) {
                        if ($client->resourceId == $receiver_connection_id[0]->connection_id) {
                            $client->send(json_encode($receiver_data));
                        }
                    }
                } else {
                    $chat = Chat::select('id', 'from_user_id', 'to_group_id', 'chat_message', 'message_status')
                        ->with('users')
                        ->where('to_group_id', $group[0]['id'])
                        ->orderBy('id', 'ASC')
                        ->get();
                    $send_data['chat_history'] = $chat;
                    $send_data['to_group_id'] = $group[0]['id'];

                    $receiver_connection_id = User::select('connection_id')->where('id', $data->from_user_id)->get();

                    foreach ($this->clients as $client) {
                        if ($client->resourceId == $receiver_connection_id[0]->connection_id) {
                            $client->send(json_encode($send_data));
                        }
                    }
                }
            }
            if ($data->type == 'request_list_user') {
                $list_user = User::where('id', '!=', $data->from_user_id)->get();
                $send_data['data'] = $list_user;
                $send_data['list_user_add_group'] = true;

                $send_connection_id = User::select('connection_id')->where('id', $data->from_user_id)->get();
                foreach ($this->clients as $client) {
                    if ($client->resourceId == $send_connection_id[0]->connection_id) {
                        $client->send(json_encode($send_data));
                    }
                }
            }
            if ($data->type == 'request_save_group_of_user') {
                $group = Group::create([
                    'name' => $data->name_group,
                    'group_status' => 'Groups',
                ]);
                $params = [];
                foreach ($data->arr_user as $value) {
                    $params[] = [
                        'user_id' => $value,
                        'group_id' => $group->id,
                        'created_at' => now(),
                        'updated_at' => now(),
                    ];
                }
                UserOfGroup::insert($params);
                $arrConnectionId = User::whereIn('id', $data->arr_user)->pluck('connection_id');
                foreach ($this->clients as $client) {
                    if (in_array($client->resourceId, $arrConnectionId->toArray())) {
                        $detailUser = User::where('connection_id', $client->resourceId)->first();
                        $group = Group::select('id', 'name', 'group_status')
                            ->with('user_of_group')
                            ->whereHas('user_of_group.users', function ($query) use ($detailUser) {
                                $query->where('id', $detailUser->id);
                            })->get();

                        $receiver_data['data'] = $group;

                        $receiver_data['response_group_user'] = true;
                        $client->send(json_encode($receiver_data));
                    }
                }
            }
        }
    }
}

Bước 5 : Tạo tập tin View Blades

Trong bước này, chúng ta phải tạo tệp Blade views để hiển thị đầu ra HTML trong trình duyệt. Vì vậy, trước tiên chúng ta phải tạo mẫu Đăng nhập, Đăng ký và Chủ cho Ứng dụng Trò chuyện Laravel này

resources/views/main.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Laravel 9 Custom Login Registration</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>

    <nav class="navbar navbar-light navbar-expand-lg mb-5" style="background-color: #e3f2fd;">
        <div class="container">
            <a class="navbar-brand mr-auto" href="#">Chat Application</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                    
                <ul class="navbar-nav">
                    @guest

                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('login') }}">Login</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('registration') }}">Register</a>
                    </li>

                    @else

                    <li class="nav-item">
                        @if(Auth::user()->user_image != '')
                        <a class="nav-link" href="#"><b>Welcome <img src="{{ asset('images/' . Auth::user()->user_image ) }}" width="35" class="rounded-circle" />&nbsp; {{ Auth::user()->name }}</b></a>
                        @else
                        <a class="nav-link" href="#"><b>Welcome <img src="{{ asset('images/no-image.jpg') }}" width="35" class="rounded-circle" />&nbsp;{{ Auth::user()->name }}</b></a>
                        @endif
                    </li>

                    {{-- <li class="nav-item">
                        <a class="nav-link" href="{{ route('profile') }}">Profile</a>
                    </li> --}}

                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('logout') }}">Logout</a>
                    </li>

                    @endguest
                </ul>
                
            </div>
        </div>
    </nav>
    <div class="container-fluid mt-5">

        @yield('content')
        
    </div>
    
</body>
</html>

resources\views\login.blade.php

@extends('main')

@section('content')

@if($message = Session::get('success'))

<div class="alert alert-info">
{{ $message }}
</div>

@endif

<div class="row justify-content-center">
	<div class="col-md-4">
		<div class="card">
			<div class="card-header">Login</div>
			<div class="card-body">
				<form action="{{ route('sample.validate_login') }}" method="post">
					@csrf
					<div class="form-group mb-3">
						<input type="text" name="email" class="form-control" placeholder="Email" />
						@if($errors->has('email'))
							<span class="text-danger">{{ $errors->first('email') }}</span>
						@endif
					</div>
					<div class="form-group mb-3">
						<input type="password" name="password" class="form-control" placeholder="Password" />
						@if($errors->has('password'))
							<span class="text-danger">{{ $errors->first('password') }}</span>
						@endif
					</div>
					<div class="d-grid mx-auto">
						<button type="subit" class="btn btn-dark btn-block">Login</button>
					</div>
				</form>
			</div>
		</div>
	</div>
</div>

@endsection('content')

resources\views\registration.blade.php

@extends('main')

@section('content')

<div class="row justify-content-center">
	<div class="col-md-4">
		<div class="card">
		<div class="card-header">Registration</div>
		<div class="card-body">
			<form action="{{ route('sample.validate_registration') }}" method="POST">
				@csrf
				<div class="form-group mb-3">
					<input type="text" name="name" class="form-control" placeholder="Name" />
					@if($errors->has('name'))
						<span class="text-danger">{{ $errors->first('name') }}</span>
					@endif
				</div>
				<div class="form-group mb-3">
					<input type="text" name="email" class="form-control" placeholder="Email Address" />
					@if($errors->has('email'))
						<span class="text-danger">{{ $errors->first('email') }}</span>
					@endif
				</div>
				<div class="form-group mb-3">
					<input type="password" name="password" class="form-control" placeholder="Password" />
					@if($errors->has('password'))
						<span class="text-danger">{{ $errors->first('password') }}</span>
					@endif
				</div>
				<div class="d-grid mx-auto">
					<button type="submit" class="btn btn-dark btn-block">Register</button>
				</div>
			</form>
		</div>
	</div>
</div>

@endsection('content')

resources\views\welcome.blade.php

<!DOCTYPE html>
{{-- <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> --}}

<head>
    <meta charset="utf-8">
    {{-- <meta name="viewport" content="width=device-width, initial-scale=1"> --}}

    <title>Laravel</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
        integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">

    <link rel="stylesheet" href="{{ asset('css/style.css') }}">
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
        integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous">
    </script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous">
    </script>
</head>

<body>
    <div class="chat-container">
        <!-- Modal -->
        <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
            aria-hidden="true">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="exampleModalLabel">Tạo nhóm mới</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <div class="form-group">
                            <label for="">Đặt Tên Nhóm</label>
                            <input type="text" class="form-control border" name="" id="name_group"
                                aria-describedby="helpId" placeholder="">
                        </div>
                        <div>
                            <ul class="people-list" id="list_user_add_group">
                            </ul>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary" data-dismiss="modal" onclick="save_group_of_user()">Tạo nhóm</button>
                    </div>
                </div>
            </div>
        </div>
        <div class="sidebar">
            <b>Đăng nhập với {{ Auth::user()->name }}</b>
            <div class="input-group mb-3">
                <input type="text" class="form-control" placeholder="Search" id="search_people"
                    onkeyup="search_user('{{ Auth::id() }}', this.value);">
                <div class="input-group-append">
                    <button class="btn btn-success" type="submit" onclick="addGroup()" data-toggle="modal"
                        data-target="#exampleModal">Add</button>
                </div>
            </div>
            <ul class="people-list" id="people-list">
                <li class="person active">
                    <img src="https://s1.zerochan.net/lost.jpg" alt="Avatar">
                    <span>John Doe</span>
                </li>
                <li class="person">
                    <img src="https://s1.zerochan.net/lost.jpg" alt="Avatar">
                    <span>Jane Doe</span>
                </li>
                <li class="person">
                    <img src="https://s1.zerochan.net/lost.jpg" alt="Avatar">
                    <span>Bob Smith</span>
                </li>
            </ul>
        </div>
        <div class="chat">
            <div class="chat-header" id="chat_header">
                <h2>Chat với ai đó</h2>
            </div>
            <div class="message-container" id="message-container">
                <div class="message received">
                    <small class="text-wrap">aaaaa</small>
                    <p>Hello!</p>
                </div>
                <div class="message sent">
                    <small>bbbbbb</small>
                    <p>Hi there!</p>
                </div>
            </div>
            <div class="chat-footer" id="chat-footer">
                <input type="text" placeholder="Type your message here">
                <button class="send_message">Send</button>
            </div>
        </div>
    </div>
</body>

</html>

<script>
    var conn = new WebSocket('ws://192.168.1.57:8090/?token={{ auth()->user()->token }}');

    var from_user_id = "{{ Auth::user()->id }}";
    var group_id = '';
    var from_user_name = "{{ Auth::user()->name }}";
    conn.onopen = function(e) {

        // console.log(e);

        showchatroom(from_user_id);

    };
    conn.onmessage = function(e) {
        var data = JSON.parse(e.data);
        if (data.response_search_user || data.response_group_user) {
            var html = '';

            if (data.data.length > 0) {
                for (var count = 0; count < data.data.length; count++) {
                    // console.log(data.data[count]);
                    if (data.data[count].group_status == 'Private') {
                        let name = data.data[count].name.replace(from_user_name, '').replace(',', '');
                        html +=
                            `<li class="person" onclick="make_chat_area(` + data.data[count].id + `, '` + name + `')">
                            <img src="https://s1.zerochan.net/lost.jpg" alt="Avatar">
                            <span>` + name + `</span>
                        </li>`
                    } else {
                        html +=
                            `<li class="person" onclick="make_chat_area(` + data.data[count].id + `, '` + data.data[
                                count].name + `')">
                            <img src="https://s1.zerochan.net/lost.jpg" alt="Avatar">
                            <span>` + data.data[count].name + `</span>
                        </li>`
                    }
                }
                document.getElementById('people-list').innerHTML = html;
            }
        }
        if (data.chat_history) {
            var html = '';
            var inputSend = `<input id="message" type="text" placeholder="Type your message here">
                                <button class="send_message" onclick="chat_message(` + data.to_group_id + `, '` +
                from_user_id + `')"">Send</button>`;
            for (var count = 0; count < data.chat_history.length; count++) {
                if (data.chat_history[count].from_user_id == from_user_id) {
                    html += `
                        <div class="message sent">
                            <small>Tôi</small>
                            <p>` + data.chat_history[count].chat_message + `</p>
                        </div>
                    `;
                } else {
                    html += `
                        <div class="message received">
                            <small>` + data.chat_history[count].users.name + `</small>
                            <p>` + data.chat_history[count].chat_message + `</p>
                        </div>
                    `;
                }
            }
            document.querySelector('#message-container').innerHTML = html;
            document.querySelector('#chat-footer').innerHTML = inputSend;

            scroll_top();
        }
        if (data.message) {
            if (group_id == data.message.to_group_id) {
                var html = '';
                if (data.message.from_user_id == from_user_id) {
                    html += `
                        <div class="message sent">
                            <small>Tôi</small>
                            <p>` + data.message.chat_message + `</p>
                        </div>
                    `;
                } else {
                    html += `
                        <div class="message received">
                            <small>` + data.message.users.name + `</small>
                            <p>` + data.message.chat_message + `</p>
                        </div>
                    `;
                }
                if (html != '') {
                    var previous_chat_element = document.querySelector('#message-container');

                    var chat_history_element = document.querySelector('#message-container');

                    chat_history_element.innerHTML = previous_chat_element.innerHTML + html;
                    scroll_top();
                }
            }
        }

        if (data.add_new_group) {
            var html = `<li class="person" onclick="make_chat_area(` + data.data[0].id + `, '` + data.data[
                0].name + `')">
                            <img src="https://s1.zerochan.net/lost.jpg" alt="Avatar">
                            <span>` + data.data[count].name + `</span>
                        </li>`;
            var previous_chat_element = document.querySelector('#people-list');

            var chat_history_element = document.querySelector('#people-list');

            chat_history_element.innerHTML = previous_chat_element.innerHTML + html;
        }
        if (data.list_user_add_group) {
            if (data.data.length > 0) {
                let html = '';
                for (var count = 0; count < data.data.length; count++) {
                    html +=
                        `<li class="person">
                            <img src="https://s1.zerochan.net/lost.jpg" alt="Avatar">
                            <span>` + data.data[count].name + `</span>
                            <input class="form-check-input" type="checkbox" value="` + data.data[count].id + `" id="flexCheckDefault">
                        </li>`
                }
                document.getElementById('list_user_add_group').innerHTML = html;
            }
        }
    }

    function load_unconnected_user(from_user_id) {
        var data = {
            from_user_id: from_user_id,
            type: 'request_load_unconnected_user'
        };

        conn.send(JSON.stringify(data));
    }

    function search_user(from_user_id, search_query) {
        if (search_query.length > 0) {
            var data = {
                from_user_id: from_user_id,
                search_query: search_query,
                type: 'request_search_user'
            };

            conn.send(JSON.stringify(data));
        } else {
            var data = {
                from_user_id: from_user_id,
                type: 'room_chat_user'
            };
            conn.send(JSON.stringify(data));
        }
    }

    function showchatroom(from_user_id) {
        var data = {
            from_user_id: from_user_id,
            type: 'room_chat_user'
        };
        conn.send(JSON.stringify(data));
    }

    function make_chat_area(id, name) {
        group_id = id;
        const lis = document.querySelectorAll('li.person');
        for (let i = 0; i < lis.length; i++) {
            lis[i].addEventListener('click', function() {
                for (let j = 0; j < lis.length; j++) {
                    lis[j].classList.remove('active');
                }
                this.classList.add('active');
            });
        }
        document.getElementById('chat_header').innerHTML = '<h2>Chat với <b>' + name + '</b></h2>';
        let search_people = document.getElementById('search_people').value;
        if (search_people.length > 0) {
            var data = {
                to_user_id: id,
                from_user_id: from_user_id,
                name_to_user_id: name,
                name_from_user_id: "{{ Auth::user()->name }}",
                type: 'create_private_chat'
            };
            console.log(search_people);
            conn.send(JSON.stringify(data));
        } else {
            var data = {
                group_chat_id: id,
                from_user_id: from_user_id,
                type: 'history_group_chat'
            };
            conn.send(JSON.stringify(data));
        }

    }

    function scroll_top() {
        document.querySelector('#message-container').scrollTop = document.querySelector('#message-container')
            .scrollHeight;
    }

    function chat_message(to_group_id, from_user_id) {
        let message = document.getElementById('message').value;
        var data = {
            to_group_id: to_group_id,
            from_user_id: from_user_id,
            chat_message: message,
            type: 'request_send_message'
        };
        conn.send(JSON.stringify(data));

        document.getElementById('message').value = '';
    }

    function addGroup() {
        document.getElementById('name_group').value = '';
        var data = {
            from_user_id: from_user_id,
            type: 'request_list_user'
        };
        conn.send(JSON.stringify(data));
    }

    function save_group_of_user() {
        let selected = [];
        $("input[type=checkbox]:checked").each(function() {
            selected.push(Number(this.value));
        });

        let name_group = document.getElementById('name_group').value;

        if (name_group.length > 0 && selected.length > 0) {
            selected.push(Number(from_user_id));
            let data = {
                arr_user: selected,
                name_group: name_group,
                type: 'request_save_group_of_user'
            };
            conn.send(JSON.stringify(data));
        } else {
            alert('hãy đặt tên nhóm và chọn thành viên');
        }
    }
</script>

Chú ý tại đây địa chỉ ip máy mình là 192.168.1.57 nên đường link ws sẽ là ws://192.168.1.57:8090/?token={{ auth()->user()->token }} hãy sửa theo địa chỉ máy bạn nhé nếu chạy trên local mà bạn đã chạy lệnh php artisan serve thì có thể thay 192.168.1.57 thành 127.0.0.1 cũng được

Bước 6: Cài đặt router cho controller

Tiếp theo, chúng ta phải thiết lập lộ trình của phương thức lớp Bộ điều khiển. Vì vậy, để làm được điều này, chúng ta phải mở routes/web.php và bên dưới tệp này, chúng ta phải viết đoạn mã sau để thiết lập tuyến đường cho phương thức Controllers Class.

routes/web.php

<?php

use App\Http\Controllers\SampleController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

// Route::get('/', function () {
//     return view('welcome');
// });
Route::controller(SampleController::class)->group(function () {
    Route::get('login', 'index')->name('login');

    Route::get('registration', 'registration')->name('registration');

    Route::get('logout', 'logout')->name('logout');

    Route::post('validate_registration', 'validate_registration')->name('sample.validate_registration');

    Route::post('validate_login', 'validate_login')->name('sample.validate_login');
    Route::middleware('auth')->group(function () {
        Route::get('/', 'welcome')->name('welcome');
    });
});

Bước 8: Chạy ứng dụng chat của mình

Đầu tiên chạy server ảo php

php artisan serve

sau đó chạy server Websocket

php artisan websocket:init

Tiếp theo hãy vào đường link http://127.0.0.1:8000/login để thử nhé