How to use Laravel Reverb

How to use Laravel Reverb

07.09.2024
Author: ADMIN

Laravel Reverb là một máy chủ WebSocket chính thức cho các ứng dụng Laravel, cung cấp khả năng giao tiếp theo thời gian thực giữa máy khách và máy chủ một cách liền mạch.

Laravel Reverb có nhiều tính năng hấp dẫn, bao gồm:

  • Tốc độ và khả năng mở rộng.
  • Hỗ trợ hàng nghìn kết nối đồng thời.
  • Tích hợp với các tính năng phát sóng hiện có của Laravel.
  • Tương thích với Laravel Echo.
  • Tích hợp và triển khai hàng đầu với Laravel Forge.

Trong hướng dẫn này, tôi sẽ hướng dẫn bạn cách sử dụng Laravel Reverb để phát triển một ứng dụng Laravel thời gian thực. Bạn sẽ học về các kênh, sự kiện, phát sóng, và cách sử dụng Laravel Reverb để tạo ra các ứng dụng nhanh chóng và thời gian thực trong Laravel.

Hơn nữa, bạn sẽ học cách thêm thông báo thời gian thực vào ứng dụng Laravel Reverb của mình!

  1. Cài đặt một ứng dụng Laravel mới
    composer create-project laravel/laravel laravel-reverb-chat

    Chuyển đến thư mục dự án của bạn:

    cd laravel-reverb-chat
  2. Installing Laravel Reverb
    Cài đặt Laravel Reverb bằng cách chạy câu lệnh sau:
    php artisan install:broadcasting
    npm install --save laravel-echo pusher-js

    Chú ý: các lựa chọn default

    Sau khi bạn đã cài đặt Reverb, bạn có thể chỉnh sửa cấu hình của nó từ tệp `config / reverb.php`. Để thiết lập kết nối đến Reverb, một bộ thông tin đăng nhập ứng dụng Reverb phải được trao đổi giữa máy khách và máy chủ. Những thông tin đăng nhập này được cấu hình trên máy chủ và được sử dụng để xác minh yêu cầu từ máy khách. Bạn có thể xác định những thông tin đăng nhập này bằng cách sử dụng các biến môi trường sau:

    BROADCAST_DRIVER=reverb
    REVERB_APP_ID=my-app-id
    REVERB_APP_KEY=my-app-key
    REVERB_APP_SECRET=my-app-secret
  3. Running Server
    Bạn có thể khởi chạy máy chủ Reverb bằng cách sử dụng command reverb:start:
    php artisan reverb:start

    Mặc định, máy chủ Reverb sẽ được khởi động tại 0.0.0.0:8080, điều này làm cho nó có thể truy cập từ tất cả các giao diện mạng. Nếu bạn muốn thiết lập một máy chủ hoặc cổng cụ thể, bạn có thể sử dụng các tùy chọn --host--port khi khởi động máy chủ.

    php artisan reverb:start --host=127.0.0.1 --port=9000

    Bạn cũng có thể xác định các biến môi trường REVERB_SERVER_HOST REVERB_SERVER_PORT trong tệp cấu hình .env của ứng dụng của bạn.

  4. Setup Database
    Mở tệp .env của bạn và điều chỉnh cài đặt để thiết lập cơ sở dữ liệu của bạn. Dưới đây là một ví dụ sử dụng MySQL để đơn giản hóa:
    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=laravel
    DB_USERNAME=root
    DB_PASSWORD=

    Đối với bản demo này, chúng ta sẽ tạo năm phòng được xác định trước. Hãy bắt đầu bằng cách tạo một migration cho một bảng rooms.

    php artisan make:model Room --migration

    Để đơn giản hóa, chỉ cần tạo trường name cho model này và chạy migration.

    Schema::create('rooms', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
    php artisan migrate

    Sau đó, seed dữ liệu cho database với 5 room. Tạo một seeder:

    php artisan make:seeder RoomsTableSeeder 
    <?php
    
    namespace Database\Seeders;
    
    use Illuminate\Database\Seeder;
    use Illuminate\Support\Facades\DB;
    
    class RoomsTableSeeder extends Seeder
    {
        /**
         * Run the database seeds.
         */
        public function run(): void
        {
            DB::table('rooms')->insert([
                ['name' => 'Room 1'],
                ['name' => 'Room 2'],
                ['name' => 'Room 3'],
                ['name' => 'Room 4'],
                ['name' => 'Room 5'],
            ]);
        }
    }
    
    Run seeder:
    php artisan db:seed --class=RoomsTableSeeder
  5. Tạo Event
    Trong thư mục app/Events, tạo một file mới tên là MessageSent.php. File này chịu trách nhiệm phát sóng các tin nhắn mới tới các phòng chat cụ thể. Dưới đây là mẫu cơ bản:
    php artisan make:event MessageSent

    class MessageSent implements ShouldBroadcast
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;
    
        public $userName;
        public $roomId;
        public $message;
    
        public function __construct($userName, $roomId, $message)
        {
            $this->userName = $userName;
            $this->roomId = $roomId;
            $this->message = $message;
        }
    
        public function broadcastOn() : Channel
        {
    
            return new Channel('chat.' . $this->roomId);
        }
    
        public function broadcastWith()
        {
            return [
                'userName' => $this->userName,
                'message' => $this->message,
            ];
        }
    }
  6. Tạo Pages
    Trong dự án này, chúng ta sẽ có hai trang: một trang để hiển thị danh sách các phòng và một trang cho từng phòng chat riêng biệt. Chúng ta sẽ bắt đầu bằng cách tạo các template Blade để hiển thị các phòng. Đặt tên các view này là index.blade.phpchat.blade.php và lưu chúng trong thư mục rooms dưới resources/views. Tiếp theo, chúng ta sẽ tạo một controller và một route để điều hướng tới các trang này.
    php artisan make:view rooms/index
    php artisan make:view rooms/chat

    index.blade.php
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>Chat Rooms</title>
    </head>
    
    <body>
        <div id="app">
            <h1>Chat Rooms</h1>
            <ul>
                @foreach ($rooms as $room)
                    <li>
                        <a href="{{ route('rooms.show', $room->id) }}">Join {{ $room->name }}</a>
                    </li>
                @endforeach
            </ul>
        </div>
    </body>
    
    </html>
    

    chat.blade.php

    Thiết lập một form cơ bản để hiển thị các tin nhắn và một trường nhập liệu đơn giản để gửi tin nhắn. Đảm bảo rằng bạn đã import Echo và Pusher vào file app.js.

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Chat Room: {{ $room->name }}</title>
        @vite(['resources/css/app.css'])
        @vite(['resources/js/app.js'])
    </head>
    
    <body>
        <div id="app">
            <h2>Chat Room: {{ $room->name }}</h2>
            <div id="messages"
                style="border: 1px solid #ccc; margin-bottom: 10px; padding: 10px; height: 300px; overflow-y: scroll;">
                <!-- Messages will be displayed here -->
            </div>
            <input type="text" id="messageInput" placeholder="Type your message here..." autofocus>
            <button onclick="sendMessage()">Send</button>
        </div>
    
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                const roomId = "{{ $room->id }}";
                Echo.channel(`chat.${roomId}`)
                    .listen('MessageSent', (e) => {
                        const messages = document.getElementById('messages');
                        const messageElement = document.createElement('div');
                        messageElement.innerHTML = `<strong>${e.userName}:</strong> ${e.message}`;
                        messages.appendChild(messageElement);
                        messages.scrollTop = messages.scrollHeight; // Scroll to the bottom
                    });
            })
    
            function sendMessage() {
                const messageInput = document.getElementById('messageInput');
                const message = messageInput.value;
                messageInput.value = ''; // Clear input
                const roomId = "{{ $room->id }}"
                fetch(`/rooms/${roomId}/message`, {
                    method: 'POST',
                    headers: {
                        'X-CSRF-TOKEN': '{{ csrf_token() }}',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        message: message
                    })
                }).catch(error => console.error('Error:', error));
            }
        </script>
    </body>
    
    </html>
    

    app.js

    import Echo from 'laravel-echo';
    
    import Pusher from 'pusher-js';
    window.Pusher = Pusher;
    
    window.Echo = new Echo({
        broadcaster: 'reverb',
        key: import.meta.env.VITE_REVERB_APP_KEY,
        wsHost: import.meta.env.VITE_REVERB_HOST,
        wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
        wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
        forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
        enabledTransports: ['ws', 'wss'],
    });

    Tiếp theo hãy tạo Controller

    php artisan make:controller RoomsController
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Room;
    
    class RoomsController extends Controller
    {
        public function index()
        {
            $rooms = Room::all();
            return view('rooms.index', [
                'rooms' => $rooms
            ]);
        }
    
        public function show(Room $room)
        {
            return view('rooms.chat', [
                'roomId' => $room->id,
                'room' => $room,
                'messages' => []
            ]);
        }
    }
    

    Để đơn giản, hãy tạo một endpoint postMessage và thêm nó vào web.php.

    php artisan make:controller ChatController
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Events\MessageSent;
    use Illuminate\Http\Request;
    use Illuminate\Support\Str;
    
    class ChatController extends Controller
    {
        public function postMessage(Request $request, $roomId)
        {
            $userName = 'User_' . Str::random(4);
            $messageContent = $request->input('message');
            MessageSent::dispatch($userName, $roomId, $messageContent);
            return response()->json(['status' => 'Message sent successfully.']);
        }
    }
    
    routes/web.php
    Route::get('/rooms', [RoomsController::class, 'index'])->name('rooms.index');
    Route::get('/rooms/{room}', [RoomsController::class, 'show'])->name('rooms.show');
    Route::post('/rooms/{roomId}/message', [ChatController::class, 'postMessage'])->name('api.rooms.message.post');
  7. Run Project
    Để chạy dự án Laravel, chúng ta cần thực thi các lệnh sau:
    - Khởi động Laravel:
    php artisan serve
    

    - Khởi động frontend:

    npm run dev
    

    - Bắt đầu queue:

    php artisan queue:listen
    

    - Run Reverb:

    php artisan reverb:start
    

Để biết thông tin chi tiết hơn, bạn có thể kiểm tra tài liệu hướng dẫn chính thức của Laravel Reverb. Chúc các bạn thành công

Ví dụ về các vấn đề truy vấn N+1

Ví dụ về các vấn đề truy vấn N+1

01.08.2024
Author: ADMIN

# Vấn Đề N+1 Query Trong Eloquent

Vấn đề N+1 query là một trong những vấn đề phổ biến mà các nhà phát triển gặp phải khi làm việc với ORM như Eloquent trong Laravel. Vấn đề này xảy ra khi ứng dụng của bạn thực hiện một lượng lớn các truy vấn nhỏ lặp đi lặp lại để lấy dữ liệu liên quan, thay vì chỉ thực hiện một vài truy vấn lớn hơn, dẫn đến giảm hiệu suất ứng dụng. Dưới đây là bốn ví dụ về vấn đề N+1 query và cách giải quyết chúng.

# Laravel Debugbar

Trước khi vào các ví dụ cụ thể, bạn nên cài đặt Laravel Debugbar để có thể kiểm tra  và debug các ứng dụng Laravel một cách dễ dàng. Debugbar hiển thị các thông tin chi tiết về các truy vấn cơ sở dữ liệu, các biến session, các yêu cầu HTTP và nhiều thông tin khác ngay trên giao diện của ứng dụng.

composer require barryvdh/laravel-debugbar --dev

Tiếp theo, bạn chỉ cần kích hoạt chế độ gỡ lỗi với biến APP_DEBUG=true trong file .env

// .env
APP_DEBUG=true 

Ví Dụ 1: Lấy Dữ Liệu Liên Quan Trong Vòng Lặp

Mã Gặp Vấn Đề N+1 Query:
$posts = Post::all();

foreach ($posts as $post) {
    echo $post->user->name;
}
Giải Thích:

Đoạn mã trên sẽ thực hiện một truy vấn để lấy tất cả các bài viết. Sau đó, nó sẽ thực hiện một truy vấn riêng lẻ cho mỗi bài viết để lấy thông tin người dùng liên quan, dẫn đến tổng cộng 1 + N truy vấn (N là số lượng bài viết).

Giải Quyết Sử Dụng Eager Loading:
$posts = Post::with('user')->get();

foreach ($posts as $post) {
    echo $post->user->name;
}
Giải Thích:

Bằng cách sử dụng phương thức with, chúng ta chỉ thực hiện hai truy vấn: một truy vấn để lấy tất cả các bài viết và một truy vấn để lấy tất cả các người dùng liên quan.

Ví Dụ 2: Ký hiệu

Giả sử bạn có cùng mối quan hệ hasMany giữa tác giả và sách và bạn cần liệt kê các tác giả cùng số lượng sách của mỗi tác giả.

// Controller

public function index()
{
    $authors = Author::with('books')->get();
 
    return view('authors.index', compact('authors'));
}

Và sau đó, trong tệp Blade, bạn thực hiện một vòng lặp foreach cho bảng:

@foreach($authors as $author)
    <tr>
        <td>{{ $author->name }}</td>
        <td>{{ $author->books()->count() }}</td>
    </tr>
@endforeach

Mọi thứ trông có vẻ hợp lý, đúng không? Nhưng nhìn vào dữ liệu Debugbar bên dưới.

Chúng ta đang sử dụng eager loading, Author::with('books'), nhưng tại sao lại có nhiều truy vấn xảy ra như thế?

Bởi vì, trong Blade,  $author->books()->count() không load mối quan hệ từ bộ nhớ.

  • $author->books() có nghĩa là PHƯƠNG THỨC của mối quan hệ
  • $author->books có nghĩa là DỮ LIỆU được eager loaded vào bộ nhớ

Vậy, phương pháp liên kết sẽ truy vấn cơ sở dữ liệu cho mỗi tác giả. Nhưng nếu bạn tải dữ liệu mà không có dấu ngoặc "()", nó sẽ thành công sử dụng dữ liệu được eager loading:

Vì vậy, hãy chú ý đến chính xác những gì bạn đang sử dụng - phương pháp quan hệ hay dữ liệu.

Lưu ý rằng trong ví dụ cụ thể này có một giải pháp thậm chí còn tốt hơn. Nếu bạn chỉ cần dữ liệu tổng hợp được tính toán của mối quan hệ, mà không cần mô hình đầy đủ, thì bạn chỉ nên tải các tổng hợp, như withCount:

// Controller:
$authors = Author::withCount('books')->get();
 
// Blade:
{{ $author->books_count }}

Kết quả sẽ chỉ có MỘT truy vấn đến cơ sở dữ liệu, thậm chí không phải là hai truy vấn. Và bộ nhớ cũng sẽ không bị "polluted" với dữ liệu quan hệ, do đó cũng tiết kiệm được một số RAM.

Ví Dụ 3: Mối quan hệ "ẩn" trong Accessor

Hãy lấy một ví dụ tương tự: danh sách các tác giả, với cột cho biết tác giả có hoạt động hay không: "Có" hoặc "Không". Hoạt động đó được xác định bởi việc tác giả có ít nhất một cuốn sách hay không và được tính như một accessor bên trong mô hình Author.

// Controller:
public function index()
{
    $authors = Author::all();
 
    return view('authors.index', compact('authors'));
}

// Blade file
@foreach($authors as $author)
    <tr>
        <td>{{ $author->name }}</td>
        <td>{{ $author->is_active ? 'Yes' : 'No' }}</td>
    </tr>
@endforeach

"is_active" được định nghĩa trong model Eloquent:

use Illuminate\Database\Eloquent\Casts\Attribute;
 
class Author extends Model
{
    public function isActive(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->books->count() > 0,
        );
    }
}

Hãy xem Debugbar hiển thị những gì:

Đúng, chúng ta có thể giải quyết bằng  eager loading các sách trong Controller. Nhưng trong trường hợp này, lời khuyên chung của tôi là tránh sử dụng các mối quan hệ trong accessor . Bởi vì accessor thường được sử dụng khi hiển thị dữ liệu và trong tương lai, người khác có thể sử dụng accessor này trong một số tệp Blade khác và bạn sẽ không kiểm soát được Controller đó trông như thế nào.

Nói cách khác, Accessor được cho là một phương pháp có thể tái sử dụng để định dạng dữ liệu, do đó bạn không kiểm soát được khi nào/cách thức dữ liệu sẽ được tái sử dụng. Trong trường hợp hiện tại của bạn, bạn có thể tránh truy vấn N+1, nhưng trong tương lai, người khác có thể không nghĩ đến nó.

# Giải pháp tích hợp chống lại truy vấn N + 1
Add eloquent strict loading mode