- 1. Laravel Reverb คืออะไร
- 2. สิ่งที่ต้องมี
- 3. สร้างโปรเจกต์และติดตั้ง Reverb
- 4. สร้าง Model และ Migration
- 5. สร้าง Broadcasting Event
- 6. สร้าง Controller
- 7. กำหนด Routes
- 8. เชื่อมต่อ Laravel Echo
- 9. สร้างหน้าแชต Blade
- 10. รันระบบ
- 11. การทดสอบ
- 12. แนวทางต่อยอด Production
- 13. ปัญหาที่พบบ่อย
- สรุป
- เอกสารอ้างอิง
เป้าหมาย: สร้างแชตแบบห้องสาธารณะด้วย Laravel, Reverb, Broadcasting, Laravel Echo และ Blade โดยไม่ต้องรีเฟรชหน้าเว็บ
#1. Laravel Reverb คืออะไร
Laravel Reverb เป็น WebSocket server ของ Laravel สำหรับส่งเหตุการณ์จากฝั่งเซิร์ฟเวอร์ไปยังเบราว์เซอร์แบบ real-time ผ่านระบบ Broadcasting เหมาะกับแชต การแจ้งเตือน Dashboard และการติดตามสถานะงาน
สถาปัตยกรรม
flowchart LR
A[Browser A] -->|POST /messages| B[Laravel API]
B --> C[(Database)]
B -->|Broadcast event| D[Laravel Reverb]
D -->|WebSocket| A
D -->|WebSocket| E[Browser B]
#2. สิ่งที่ต้องมี
- PHP และ Composer ที่รองรับ Laravel เวอร์ชันที่ติดตั้ง
- Node.js และ npm
- SQLite หรือฐานข้อมูลอื่น
- Terminal อย่างน้อย 3 หน้าต่าง
#3. สร้างโปรเจกต์และติดตั้ง Reverb
composer create-project laravel/laravel reverb-chat
cd reverb-chat
php artisan install:broadcasting --reverb
npm install
ตัวติดตั้ง Broadcasting จะเตรียม Reverb, Echo และค่าคอนฟิกที่จำเป็นตามเวอร์ชัน Laravel ที่ใช้อยู่ ตรวจสอบไฟล์ที่ตัวติดตั้งแก้ไขก่อนดำเนินการ
ตั้งค่า .env ให้ใช้ SQLite และตรวจสอบค่าที่ตัวติดตั้งสร้าง:
DB_CONNECTION=sqlite
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=local-app
REVERB_APP_KEY=local-key
REVERB_APP_SECRET=local-secret
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
ค่าตัวอย่างใช้สำหรับเครื่องพัฒนาเท่านั้น ในระบบจริงต้องใช้ secret ที่สุ่มอย่างปลอดภัย และ WSS/HTTPS
สร้างไฟล์ฐานข้อมูลหากยังไม่มี:
touch database/database.sqlite
php artisan migrate
บน Windows ให้สร้าง database/database.sqlite ด้วย File Explorer หรือ PowerShell แทน touch ได้
#4. สร้าง Model และ Migration
php artisan make:model Message -m
แก้ migration ใน database/migrations/*_create_messages_table.php:
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->string('name', 60);
$table->string('body', 500);
$table->timestamps();
});
}
ไฟล์ app/Models/Message.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
protected $fillable = ['name', 'body'];
}
php artisan migrate
#5. สร้าง Broadcasting Event
php artisan make:event MessageSent
ไฟล์ app/Events/MessageSent.php:
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Message $message) {}
public function broadcastOn(): array
{
return [new Channel('chat-room')];
}
public function broadcastAs(): string
{
return 'message.sent';
}
}
ShouldBroadcastNow เหมาะกับ Workshop เพราะส่งทันทีโดยไม่ต้องเปิด queue worker; งาน production อาจใช้ ShouldBroadcast ร่วมกับ queue เพื่อควบคุมโหลด
#6. สร้าง Controller
php artisan make:controller ChatController
ไฟล์ app/Http/Controllers/ChatController.php:
<?php
namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Models\Message;
use Illuminate\Http\Request;
class ChatController extends Controller
{
public function index()
{
return view('chat', [
'messages' => Message::latest()->take(30)->get()->reverse()->values(),
]);
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:60'],
'body' => ['required', 'string', 'max:500'],
]);
$message = Message::create($data);
broadcast(new MessageSent($message));
return response()->json($message, 201);
}
}
#7. กำหนด Routes
ไฟล์ routes/web.php:
<?php
use App\Http\Controllers\ChatController;
use Illuminate\Support\Facades\Route;
Route::get('/', [ChatController::class, 'index']);
Route::post('/messages', [ChatController::class, 'store'])
->middleware('throttle:30,1');
#8. เชื่อมต่อ Laravel Echo
ตรวจสอบไฟล์ resources/js/bootstrap.js ที่ install:broadcasting สร้าง โดยต้องมีการตั้งค่า Echo คล้ายตัวอย่างนี้:
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: Number(import.meta.env.VITE_REVERB_PORT ?? 80),
wssPort: Number(import.meta.env.VITE_REVERB_PORT ?? 443),
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
ตรวจสอบว่า resources/js/app.js import bootstrap:
import './bootstrap';
#9. สร้างหน้าแชต Blade
ไฟล์ resources/views/chat.blade.php:
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Laravel Reverb Chat</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
body { font-family: sans-serif; max-width: 720px; margin: 40px auto; padding: 0 16px; }
#messages { height: 360px; overflow-y: auto; border: 1px solid #ddd; padding: 16px; }
form { display: flex; gap: 8px; margin-top: 16px; }
input { padding: 10px; min-width: 0; }
input[name="body"] { flex: 1; }
</style>
</head>
<body>
<h1>💬 Laravel Reverb Chat</h1>
<div id="messages">
@foreach ($messages as $message)
<p><strong>{{ $message->name }}:</strong> {{ $message->body }}</p>
@endforeach
</div>
<form id="chat-form">
<input name="name" placeholder="ชื่อ" maxlength="60" required>
<input name="body" placeholder="ข้อความ" maxlength="500" required>
<button type="submit">ส่ง</button>
</form>
<script type="module">
const box = document.querySelector('#messages');
const form = document.querySelector('#chat-form');
const addMessage = (message) => {
const p = document.createElement('p');
const strong = document.createElement('strong');
strong.textContent = `${message.name}: `;
p.append(strong, document.createTextNode(message.body));
box.appendChild(p);
box.scrollTop = box.scrollHeight;
};
window.Echo.channel('chat-room')
.listen('.message.sent', (event) => addMessage(event.message));
form.addEventListener('submit', async (event) => {
event.preventDefault();
const button = form.querySelector('button');
button.disabled = true;
try {
const response = await fetch('/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
form.elements.body.value = '';
// ไม่ append ที่นี่ เพราะข้อความจะเข้ามาทาง Reverb อยู่แล้ว
} catch (error) {
alert(`ส่งข้อความไม่สำเร็จ: ${error.message}`);
} finally {
button.disabled = false;
}
});
</script>
</body>
</html>
ในบางการตั้งค่า Vite/Blade การเข้าถึง
window.Echoใน inline module อาจเกิดลำดับโหลดไม่ตรงกัน หากพบปัญหาให้ย้าย JavaScript ส่วนหน้าแชตไปไว้ในresources/js/app.jsหลังimport './bootstrap'หรือแยกเป็นไฟล์ module ที่ import จาก app.js
#10. รันระบบ
เปิด Terminal แยกกัน:
# Terminal 1: Laravel HTTP server
php artisan serve
# Terminal 2: Reverb WebSocket server
php artisan reverb:start --debug
# Terminal 3: Vite
npm run dev
เปิด http://127.0.0.1:8000 สองแท็บ ตั้งชื่อคนละชื่อ แล้วทดลองส่งข้อความ ข้อความควรแสดงทั้งสองแท็บทันที
#11. การทดสอบ
| กรณีทดสอบ | ผลที่คาดหวัง |
|---|---|
| ส่งข้อความจากแท็บ A | แท็บ A และ B ได้รับข้อความ |
| ส่งข้อความว่าง | ได้รับ HTTP 422 |
| ข้อความเกิน 500 ตัวอักษร | ได้รับ HTTP 422 |
| รีเฟรชหน้าเว็บ | แสดง 30 ข้อความล่าสุด |
| ปิด Reverb server | ส่ง HTTP ได้ แต่ไม่อัปเดตแบบ real-time |
| ส่งข้อความเป็น HTML | แสดงเป็นข้อความ ไม่ถูกประมวลผลเป็น HTML |
#12. แนวทางต่อยอด Production
- เปลี่ยนจาก Public Channel เป็น Private/Presence Channel พร้อม Authentication และ Authorization หากมีข้อมูลส่วนตัว
- ใช้ HTTPS/WSS และ reverse proxy ที่รองรับ WebSocket Upgrade
- ตั้งค่า
REVERB_ALLOWED_ORIGINSตามโดเมนที่อนุญาต - แยก Reverb process ออกจาก HTTP process และใช้ process supervisor
- พิจารณา queue worker, Redis และการ scale ตามปริมาณผู้ใช้
- เพิ่มการตรวจสอบสิทธิ์ผู้ส่ง, rate limiting, monitoring และการเก็บ log โดยไม่เปิดเผยข้อมูลลับ
- ออกแบบการ reconnect, missed messages และการดึงประวัติย้อนหลัง เพราะ WebSocket ไม่รับประกันการส่งย้อนหลังเมื่อ client หลุด
#13. ปัญหาที่พบบ่อย
- WebSocket connection failed: ตรวจสอบ
REVERB_HOST, port 8080, firewall และ Vite variables แล้ว restart Vite - Event ไม่มาถึง: ตรวจสอบ
BROADCAST_CONNECTION=reverb, ชื่อ Channel และ.listen('.message.sent', ...) - 419 Page Expired: ตรวจสอบ CSRF token ใน request
- Duplicate messages: อย่า append ข้อความทั้งจาก HTTP response และ Broadcast พร้อมกันโดยไม่ทำ deduplication
- Deploy แล้วเชื่อมต่อไม่ได้: ตรวจสอบ reverse proxy, TLS, WebSocket Upgrade และ origin restrictions
#สรุป
Laravel Reverb ช่วยให้แอป Laravel สื่อสารกับเบราว์เซอร์แบบ real-time ได้ผ่าน WebSocket Workshop นี้ใช้ Public Channel เพื่อสาธิตหลักการ จึงไม่ควรนำไปใช้กับข้อความส่วนตัวโดยไม่มีระบบยืนยันตัวตนและการกำหนดสิทธิ์
#เอกสารอ้างอิง
- Laravel Reverb: https://laravel.com/docs/reverb
- Laravel Broadcasting: https://laravel.com/docs/broadcasting
- Laravel Echo: https://github.com/laravel/echo