Xây Dựng Streaming API Bằng Laravel: Phá Vỡ Bộ Đệm PHP
Ở bài trước chúng ta đã hiểu lý thuyết về Streaming. Nhưng khi mang lý thuyết đó vào thế giới của PHP và Laravel, bạn sẽ đụng phải một "bức tường thành" cực kỳ kiên cố: Output Buffering (Bộ đệm đầu ra).
Mặc định, PHP sinh ra để gom tất cả mọi thứ bạn echo vào một cái khay (buffer), đợi script chạy xong xuôi 100% rồi mới bưng cả cái khay đó trả về cho Nginx/Apache. Để Streaming được trong Laravel, chúng ta phải "phá vỡ" cái khay này!
Dưới đây là hướng dẫn "cầm tay chỉ việc" từ số 0, giúp bạn xây dựng một API gõ chữ giống hệt ChatGPT bằng Laravel. Bật Terminal lên và chiến thôi!
BƯỚC 1: KHỞI TẠO DỰ ÁN TỪ SỐ 0
Chúng ta sẽ bắt đầu bằng những dòng lệnh cơ bản nhất. Mở Terminal (Mac/Linux) hoặc Git Bash (Windows) và gõ:
mkdir vibe-laravel-stream

cd vibe-laravel-stream

composer create-project laravel/laravel .

code .

BƯỚC 2: VIẾT ROUTE VÀ LOGIC STREAMING (BACKEND)
Trong VS Code, mở file routes/web.php.
Chúng ta không cần Controller rườm rà cho bài demo này. Viết thẳng logic vào Route.
Laravel cung cấp một phương thức tuyệt vời là response()->stream(). Nhưng điều quan trọng nhất nằm ở 2 hàm của PHP: ob_flush() và flush().
Hãy xóa hết code cũ trong web.php và dán đoạn này vào:
<?php
use Illuminate\Support\Facades\Route;
// 1. Giao diện Frontend (Mình sẽ viết ở Bước 3)
Route::get('/', function () {
return view('welcome');
});
// 2. API Streaming của Vibe Coder
Route::get('/api/stream', function () {
// Sử dụng stream() của Laravel
return response()->stream(function () {
// Đoạn văn giả lập AI sinh ra
$text = "Chào mừng anh em đến với thế giới của Vibe Coder! Streaming không chỉ là một công nghệ, nó là một nghệ thuật giúp tối ưu hóa TTFB và mang lại trải nghiệm đỉnh cao cho người dùng.";
$words = explode(' ', $text);
foreach ($words as $word) {
// In ra từng chữ, kèm theo một khoảng trắng
echo $word . ' ';
// TUYỆT KỸ PHÁ BỘ ĐỆM PHP:
// Ép PHP phải tống dữ liệu đang có ra khỏi RAM ngay lập tức
if (ob_get_level() > 0) {
ob_flush();
}
flush(); // Ép Server (Apache/Nginx) đẩy dữ liệu xuống mạng
// Tạm dừng 200ms để giả lập độ trễ tính toán của AI
usleep(200000);
}
}, 200, [
// Các Headers bắt buộc để báo cho trình duyệt biết đây là Luồng dữ liệu
'Content-Type' => 'text/plain; charset=UTF-8',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no', // Tắt buffering của Nginx (Nếu chạy trên server thật)
]);
});
BƯỚC 3: TIÊU THỤ DỮ LIỆU Ở FRONTEND (JAVASCRIPT)
Backend đã "nhỏ giọt" rồi, nhưng nếu Frontend dùng hàm await fetch().then(res => res.text()) bình thường, nó vẫn sẽ... đứng chờ cho đến khi Backend nhỏ giọt xong 100% rồi mới hiện ra!
Để Frontend nhận được từng chữ realtime, bạn phải dùng Streams API của trình duyệt.
Mở file resources/views/welcome.blade.php, xóa sạch mọi thứ và dán đoạn code HTML/JS này vào:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laravel Streaming Demo</title>
<style>
body { background-color: #1e1e2e; color: #cdd6f4; font-family: 'Courier New', Courier, monospace; padding: 50px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; background-color: #89b4fa; color: #11111b; border: none; border-radius: 5px; font-weight: bold; }
#chat-box { margin-top: 20px; padding: 20px; border: 1px solid #45475a; border-radius: 8px; min-height: 100px; background-color: #181825; font-size: 18px; line-height: 1.6; }
.cursor { display: inline-block; width: 10px; height: 20px; background-color: #a6e3a1; animation: blink 1s step-end infinite; vertical-align: bottom; margin-left: 5px;}
@keyframes blink { 50% { opacity: 0; } }
</style>
</head>
<body>
<h1>🤖 AI Assistant (Laravel Streaming)</h1>
<button id="btn-ask">Hỏi AI ngay!</button>
<div id="chat-box">
<span id="content"></span>
<span class="cursor" id="cursor" style="display: none;"></span>
</div>
<script>
document.getElementById('btn-ask').addEventListener('click', async () => {
const contentDiv = document.getElementById('content');
const cursor = document.getElementById('cursor');
const btn = document.getElementById('btn-ask');
contentDiv.innerHTML = ''; // Reset nội dung
btn.disabled = true;
btn.innerText = 'Đang suy nghĩ...';
cursor.style.display = 'inline-block';
try {
// 1. Gửi Request
const response = await fetch('/api/stream');
// 2. Lấy bộ đọc luồng (Reader)
const reader = response.body.getReader();
// Dữ liệu stream về là các byte (Uint8Array), cần công cụ dịch sang Text
const decoder = new TextDecoder('utf-8');
// 3. Vòng lặp vô tận: Đọc liên tục cho đến khi Server báo xong
while (true) {
const { done, value } = await reader.read();
if (done) {
break; // Server đã gọi res.end() (hoặc chạy hết hàm stream)
}
// Dịch byte sang chữ và in ra màn hình
const chunkText = decoder.decode(value);
contentDiv.innerHTML += chunkText;
}
} catch (error) {
console.error("Lỗi mất rồi:", error);
} finally {
btn.disabled = false;
btn.innerText = 'Hỏi AI ngay!';
cursor.style.display = 'none';
}
});
</script>
</body>
</html>
BƯỚC 4: CHẠY DEMO VÀ TẬN HƯỞNG
Mọi thứ đã sẵn sàng. Mở Terminal của VS Code (nhấn Ctrl +) và khởi động server ảo của Laravel:
php artisan serve
Server sẽ chạy ở địa chỉ http://localhost:8000.
Mở trình duyệt, truy cập vào link đó. Bạn sẽ thấy giao diện Chat.
Hãy bấm nút "Hỏi AI ngay!" và tận hưởng cảm giác từng chữ cái được "gõ" ra màn hình một cách mượt mà, không hề có độ trễ ban đầu!

Lời kết của Vibe Coder
Bạn vừa đâm thủng được lớp giáp Output Buffering cứng đầu của PHP! Sự kết hợp giữa response()->stream() của Laravel ở Backend và response.body.getReader() ở Frontend chính là chìa khóa để bạn xây dựng mọi ứng dụng giao tiếp với LLM (như OpenAI, Gemini) hoặc các hệ thống báo cáo thời gian thực.
Code chạy được rồi, hãy thử vọc vạch thay đổi thời gian usleep() hoặc thay dòng chữ cứng thành một vòng lặp truy vấn từ Database xem sao nhé! Chúc bạn code vui!
All rights reserved