Từ bài này, chúng ta nâng cấp lên các phép cộng có số từ 100 đến 999.&
Bạn vẫn chỉ cần sử dụng phép cộng (+), không cần phép trừ hay phép chia làm kỹ thuật chính.
Mục tiêu: Biết cách cộng từ trái sang phải, bù tròn hàng trăm và ghép số để tính nhanh trong đầu.
Ví dụ:
Thay vì cộng từng chữ số từ phải sang trái, hãy cộng theo từng phần:
Bước 1 · Hàng trăm
Bước 2 · Hàng chục
Bước 3 · Hàng đơn vị
Mẹo: Hãy giữ tổng tạm thời trong đầu, rồi lần lượt cộng hàng trăm, hàng chục và hàng đơn vị.
Khi một số gần 100, 200, 300 hoặc 500, hãy tận dụng số tròn.
Ví dụ:
Ta nhận thấy 197 gần bằng 200.
\(386+200=586\).
Vì 197 ít hơn 200 là 3, kết quả cần thấp hơn 586 là 3.
Vậy:
Một ví dụ khác:
Tính \(468+300=768\), rồi điều chỉnh 2 đơn vị:
Bạn chỉ cần nhớ một nguyên tắc: Làm tròn số thứ hai, sau đó điều chỉnh phần chênh lệch.
Đây là cách rất nhanh khi cộng hai số có hàng trăm và hàng chục phù hợp.
Ví dụ:
Nhìn hàng trăm và hàng chục:
\(275+25=300\).
Số 425 còn lại 400.
\(300+400=700\).
Kết quả:
Ví dụ tiếp theo:
Vì \(346+254=600\), ta có thể nhận ra ngay kết quả bằng cách ghép phần đơn vị và hàng chục để tạo thành số tròn trăm.
Ví dụ:
Tìm cặp dễ tính trước:
Sau đó:
Vậy tổng là 768.
Khi có ba số trở lên, đừng mặc định cộng theo thứ tự xuất hiện. Hãy tìm cặp tạo thành 100, 200, 500 hoặc một số tròn trăm khác trước.
Hãy tính nhẩm, không dùng máy tính. Tất cả các số hạng đều nhỏ hơn 1.000.
Nhập kết quả vào từng ô, sau đó kiểm tra đáp án.
Câu 1
Đáp án: 386. Cộng 200, rồi 30, rồi 4; tiếp tục cộng 152.
Câu 2
Đáp án: 688. 425 + 200 = 625; cộng tiếp 63.
Câu 3
Đáp án: 583. Cộng 200 rồi điều chỉnh 3.
Câu 4
Đáp án: 766. Cộng 300 rồi điều chỉnh 2.
Câu 5
Đáp án: 700. Ghép trực tiếp thành 700.
Câu 6
Đáp án: 600. Ghép thành số tròn trăm 600.
Câu 7
Đáp án: 800. 625 cần thêm 175 để thành 800.
Câu 8
Đáp án: 774. Cộng 500 rồi điều chỉnh 2.
Câu 9
Đáp án: 720. 245 + 355 = 600; cộng thêm 120.
Câu 10
Đáp án: 799. 325 + 275 = 600; cộng 199.
Hãy xem lại các câu sai và thử giải lại bằng một phương pháp khác.
Sau khi hoàn thành bài tập, hãy tự đặt mục tiêu:
Cấp độ 1: Đúng 8/10 câu.
Cấp độ 2: Đúng 10/10 câu trong 90 giây.
Cấp độ 3: Đúng 10/10 câu trong 60 giây.
Không cần vội đạt tốc độ cao ngay. Hãy luyện cho đến khi bạn tự động nhận ra số gần tròn trăm và các cặp số đẹp.
Bài 5 tiếp theo: Nhân nhẩm siêu tốc với 2, 3, 4 và 5 — bắt đầu chuyển từ phép cộng sang phép nhân.
✦ 🎓 Made by AI 🎓 ✦
Cập nhật: 2026-10-28T07:25:00.114+07:00
Bài hôm nay nâng cấp từ cộng 2 số lên cộng 3 số.&
Bí quyết quan trọng nhất là: Đừng cộng theo thứ tự đề bài.&
Hãy tìm hai số cộng lại thành số tròn chục hoặc tròn trăm trước!
Ví dụ:
Nếu cộng lần lượt, bạn phải xử lý nhiều bước. Hãy quan sát:
\(27+73=100\)
\(100+38=138\)
GHÉP CẶP THÔNG MINH
Tại sao nhanh? Vì não người xử lý số tròn như 10, 100, 200 dễ dàng hơn các số lẻ.
Ví dụ:
Nhìn thấy \(46+54=100\), ta tính:
Một ví dụ khác:
Vì \(35+65=100\), nên:
Mẹo: Khi gặp 3 số, hãy kiểm tra xem hai số nào có hàng đơn vị cộng lại bằng 10 và hàng chục có thể ghép thành 100.
Ví dụ:
Ta có thể ghép \(24+19=43\), rồi cộng 37:
Hoặc ghép \(37+19=56\), rồi cộng 24:
Cả hai đều đúng. Hãy chọn cặp cho kết quả dễ cộng tiếp nhất.
Với quy tắc khóa học giới hạn số nhỏ hơn 1.000, ta vẫn có thể sử dụng số tròn 1.000 làm kết quả trung gian, miễn là từng số đầu vào không vượt quá 999.
Ví dụ:
Ghép \(275+425=700\), sau đó \(700+300=1000\).
Kết quả là 1.000. Nếu bạn muốn giới hạn cả kết quả dưới 1.000, ta sẽ loại những bài có tổng đạt hoặc vượt 1.000 khỏi phần luyện tập.
Tìm cặp số dễ ghép trước khi nhập kết quả.
Câu 1
Đáp án: 146. Ghép 27 + 73 = 100.
Câu 2
Đáp án: 148. Ghép 35 + 65 = 100.
Câu 3
Đáp án: 118. Ghép 42 + 58 = 100.
Câu 4
Đáp án: 236. Ghép 125 + 75 = 200.
Câu 5
Đáp án: 485. Ghép 240 + 160 = 400.
Câu 6
Đáp án: 149. Ghép 67 + 33 = 100.
Câu 7
Đáp án: 278. Ghép 145 + 55 = 200.
Câu 8
Đáp án: 464. Ghép 225 + 175 = 400.
Câu 9
Đáp án: 224. Ghép 86 + 114 = 200.
Câu 10
Đáp án: 599. Ghép 315 + 185 = 500.
Hãy chú ý cả tốc độ lẫn cách ghép cặp, không chỉ kết quả.
Tìm cặp bằng 100: ví dụ \(27+73\), \(46+54\).
Tìm cặp bằng 200, 300, 400… khi làm việc với số có ba chữ số.
Đừng cố ghép bằng mọi giá: nếu không có cặp đẹp, hãy cộng hai số tạo ra kết quả dễ tính nhất.
Bài tiếp theo: Bài 4 – Cộng nhẩm số có ba chữ số, với các kỹ thuật làm tròn hàng trăm, hàng chục và ghép số để tính nhanh hơn.
✦ 🎓 Made by AI 🎓 ✦
Cập nhật: 2026-10-27T07:22:00.129+07:00
Mục tiêu hôm nay: cộng hai số từ 10 đến 99 bằng cách tách chục, tách đơn vị hoặc bù tròn.&
Bạn sẽ học cách chọn phương pháp nhanh nhất thay vì lúc nào cũng cộng từ phải sang trái.
Ví dụ:
Tách thành:
Cộng chục với chục, đơn vị với đơn vị:
Mẹo: Khi cộng hai số có hai chữ số, hãy ưu tiên cộng phần chục trước.
Đây là cách rất hữu ích khi bạn muốn tính nhanh trong đầu.
Ví dụ:
Bước 1
58 + 2 = 60
Lấy 2 từ số 27 để đưa 58 thành số tròn chục.
Bước 2
27 − 2 = 25
Số còn lại là 25.
Kết quả: \(58+27=85\).
Bạn có thể dùng cách này khi một số gần 20, 30, 40, 50, 60, 70, 80 hoặc 90.
Ví dụ:
Thay vì cộng hàng đơn vị trước, hãy làm như sau:
\(64+20=84\)
\(84+3=87\)
Vậy \(64+23=87\).
Cách này đặc biệt phù hợp khi tính nhẩm lúc đọc số liệu, xem giá cổ phiếu hoặc tính nhanh các con số trong công việc.
Dạng phép tính | Phương pháp ưu tiên |
|---|---|
42 + 35 | Tách chục, đơn vị |
58 + 27 | Bù tròn chục |
64 + 23 | Cộng từ trái sang phải |
79 + 18 | Đưa 79 thành 80 |
46 + 47 | Nhân đôi gần đúng: 46 + 46 + 1 |
Lưu ý: Không có một phương pháp nhanh nhất cho mọi phép tính. Bạn nên chọn cách có ít bước nhất trong đầu.
Tự bấm giờ nếu muốn luyện tốc độ. Nhập đáp án rồi kiểm tra kết quả.
Câu 1
Câu 2
Câu 3
Câu 4
Câu 5
Câu 6
Câu 7
Câu 8
Câu 9
Câu 10
Đáp án: 1. 59 · 2. 78 · 3. 85 · 4. 86 · 5. 82 · 6. 95 · 7. 95 · 8. 93 · 9. 103 · 10. 95
Trong ngày, hãy chọn 10 phép cộng hai số có hai chữ số mà bạn gặp trong thực tế. Với mỗi phép tính, thử tìm cách tính trong đầu trước khi dùng máy tính.
Tiêu chuẩn hoàn thành bài 2:
Tính đúng ít nhất 8/10 câu.
Không viết phép tính ra giấy.
Cố gắng hoàn thành 10 câu trong 60 giây, nhưng ưu tiên chính xác trước tốc độ.
Bài tiếp theo: Bài 3 – Cộng nhanh 3 số trong một phép tính, ví dụ \(27+38+73\), bằng cách tìm các cặp số tạo thành 100.
✦ 🎓 Made by AI 🎓 ✦
Cập nhật: 2026-10-26T07:21:00.113+07:00
Sau bài học này, bạn sẽ hiểu cách JavaScript trao đổi dữ liệu với Laravel thông qua JSON và HTTP, nền tảng quan trọng để xây dựng ứng dụng Vue + Laravel REST API.
Bạn sẽ học được:
JSON là gì và tại sao frontend cần JSON?
Chuyển đổi dữ liệu giữa JSON và JavaScript.
HTTP Request và HTTP Response hoạt động như thế nào?
Các HTTP Method: GET, POST, PUT, PATCH, DELETE.
Các HTTP Status Code phổ biến.
Xử lý lỗi khi gọi API.
Thực hành gửi và nhận dữ liệu JSON bằng fetch().
Chuẩn bị cho Bài 08: JavaScript gọi Laravel REST API.
JSON (JavaScript Object Notation) là một định dạng văn bản dùng để lưu trữ và trao đổi dữ liệu giữa các hệ thống.
Ví dụ, Laravel có thể trả về thông tin một bài viết dưới dạng JSON:
{
"id": 1,
"title": "Học Laravel 13",
"author": "Nguyen Van A",
"published": true
}JavaScript nhận dữ liệu này, sau đó hiển thị tiêu đề, tác giả và trạng thái bài viết lên giao diện.
Laravel Backend
Lấy dữ liệu từ database
HTTP Request / Response — JSON
JavaScript Frontend
Nhận dữ liệu và cập nhật giao diện
Lưu ý: JSON là định dạng dữ liệu, còn HTTP là giao thức truyền dữ liệu. Hai khái niệm này thường được sử dụng cùng nhau khi xây dựng REST API.
Hai dạng dữ liệu trông khá giống nhau nhưng không hoàn toàn giống nhau.
const post = {
id: 1,
title: "Học JavaScript",
published: true
};
console.log(post.title);Đây là một object mà JavaScript có thể sử dụng trực tiếp.
const jsonString = `{
"id": 1,
"title": "Học JavaScript",
"published": true
}`;Đây là một chuỗi văn bản có nội dung theo cú pháp JSON.
Một số quy tắc quan trọng của JSON:
Tên thuộc tính phải đặt trong dấu ngoặc kép " ".
Chuỗi ký tự phải dùng dấu ngoặc kép.
Không cho phép comment trong JSON chuẩn.
Không hỗ trợ trực tiếp các giá trị undefined, function hoặc Symbol.
Ví dụ JSON không hợp lệ:
{
title: 'Laravel 13',
// Đây là bài viết mới
published: true
}Nguyên nhân: title không được đặt trong dấu ngoặc kép và JSON không hỗ trợ comment.
JavaScript cung cấp hai phương thức quan trọng: JSON.parse() và JSON.stringify().
const jsonString = '{"id":1,"title":"Laravel 13"}';
const post = JSON.parse(jsonString);
console.log(post.id);
console.log(post.title);Kết quả:
1
Laravel 13Luồng chuyển đổi:
JSON String
Văn bản trao đổi dữ liệu
JSON.parse()
JavaScript Object
Truy cập bằng post.id, post.title
const post = {
id: 1,
title: "Học Vue",
published: true
};
const jsonString = JSON.stringify(post);
console.log(jsonString);Kết quả:
{"id":1,"title":"Học Vue","published":true}Phương thức này thường được sử dụng khi gửi dữ liệu từ JavaScript lên Laravel.
Ví dụ:
const user = {
name: "Minh",
email: "minh@example.com"
};
const body = JSON.stringify(user);Biến body chứa chuỗi JSON sẵn sàng để gửi trong HTTP Request.
Ghi nhớ nhanh:
Phương thức | Chức năng |
|---|---|
| JSON String → JavaScript Object |
| JavaScript Object → JSON String |
HTTP là giao thức giúp client và server trao đổi dữ liệu.
Trong một ứng dụng Laravel, client có thể là trình duyệt chạy JavaScript hoặc Vue. Server là ứng dụng Laravel tiếp nhận yêu cầu và trả về kết quả.
Một HTTP Request thường có:
Method: Hành động muốn thực hiện.
URL: Địa chỉ tài nguyên.
Headers: Thông tin mô tả request.
Body: Dữ liệu gửi lên, nếu có.
Một HTTP Response thường có:
Status Code: Kết quả xử lý.
Headers: Thông tin phản hồi.
Body: Dữ liệu trả về, chẳng hạn JSON.
Ví dụ một request lấy danh sách bài viết:
GET /api/posts HTTP/1.1
Accept: application/jsonVí dụ response:
HTTP/1.1 200 OK
Content-Type: application/json[
{
"id": 1,
"title": "Bài viết đầu tiên"
},
{
"id": 2,
"title": "Bài viết thứ hai"
}
]Đây là cách frontend yêu cầu Laravel cung cấp danh sách bài viết.
Method | Ý nghĩa | Ví dụ |
|---|---|---|
| Lấy dữ liệu | Xem danh sách bài viết |
| Tạo tài nguyên mới | Thêm bài viết |
| Thay thế toàn bộ tài nguyên theo quy ước API | Cập nhật toàn bộ bài viết |
| Cập nhật một phần tài nguyên | Đổi tiêu đề bài viết |
| Xóa tài nguyên | Xóa bài viết |
Ví dụ các endpoint Laravel:
GET /api/posts
GET /api/posts/1
POST /api/posts
PUT /api/posts/1
PATCH /api/posts/1
DELETE /api/posts/1Các endpoint trên là ví dụ theo quy ước REST API. Bạn cần khai báo route tương ứng ở phía Laravel để chúng hoạt động.
Status Code giúp frontend biết request đã thành công hay gặp vấn đề.
Mã | Ý nghĩa | Tình huống |
|---|---|---|
| Thành công | Lấy dữ liệu thành công |
| Đã tạo | Thêm bài viết thành công |
| Thành công, không có nội dung trả về | Xóa bài viết thành công |
| Request không hợp lệ | Dữ liệu gửi không đúng yêu cầu |
| Chưa xác thực hợp lệ | Chưa đăng nhập hoặc thông tin xác thực không hợp lệ |
| Không có quyền | User không được phép xóa bài viết |
| Không tìm thấy | Bài viết không tồn tại |
| Dữ liệu không vượt qua xử lý hoặc validation | Laravel trả lỗi validation |
| Lỗi server | Lỗi phát sinh trong backend |
Một điểm rất quan trọng: fetch() thường không tự động reject Promise khi server trả về HTTP 404 hoặc 500. Bạn cần kiểm tra response.ok hoặc response.status.
Ở Bài 06, bạn đã học fetch(). Bây giờ chúng ta kết hợp Fetch API với JSON.
async function getPosts() {
try {
const response = await fetch("/api/posts", {
headers: {
Accept: "application/json"
}
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const posts = await response.json();
console.log(posts);
} catch (error) {
console.error("Không thể tải bài viết:", error.message);
}
}
getPosts();Giải thích:
fetch() gửi HTTP Request đến /api/posts.
await chờ server trả về Response.
response.ok kiểm tra status thuộc nhóm 200–299.
response.json() đọc body và chuyển JSON thành dữ liệu JavaScript.
try...catch xử lý lỗi mạng, lỗi đọc dữ liệu hoặc lỗi do chúng ta chủ động ném ra.
Lưu ý: response.json() là một thao tác bất đồng bộ và trả về Promise. Bạn thường phải dùng await response.json().
Giả sử bạn muốn tạo một bài viết mới với tiêu đề và nội dung.
async function createPost() {
const postData = {
title: "Học Laravel 13",
content: "Bài học về JSON và HTTP"
};
try {
const response = await fetch("/api/posts", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(postData)
});
const data = await response.json();
if (!response.ok) {
console.error("API trả lỗi:", data);
return;
}
console.log("Đã tạo bài viết:", data);
} catch (error) {
console.error("Lỗi kết nối:", error.message);
}
}
createPost();Ba dòng cần nhớ nhất:
method: "POST"Cho server biết chúng ta đang gửi request tạo tài nguyên.
"Content-Type": "application/json"Cho server biết body được gửi lên có định dạng JSON.
body: JSON.stringify(postData)Chuyển JavaScript Object thành chuỗi JSON để gửi qua HTTP.
Phía Laravel vẫn cần khai báo route, kiểm tra dữ liệu, xác thực và lưu dữ liệu. Đoạn JavaScript này không tự tạo API backend.
Khi Laravel validation thất bại, API thường trả về HTTP 422 cùng dữ liệu lỗi, nếu request được xử lý theo cơ chế JSON.
Ví dụ response:
{
"message": "The title field is required.",
"errors": {
"title": [
"The title field is required."
]
}
}JavaScript có thể xử lý như sau:
async function createPost() {
const response = await fetch("/api/posts", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "",
content: "Nội dung bài viết"
})
});
const data = await response.json();
if (response.status === 422) {
console.log("Lỗi tiêu đề:", data.errors?.title);
return;
}
if (!response.ok) {
console.error("Có lỗi xảy ra:", data.message);
return;
}
console.log("Tạo bài viết thành công:", data);
}Trong dự án thực tế, bạn nên bổ sung try...catch để xử lý lỗi mạng và trường hợp body không phải JSON hợp lệ.
Toán tử ?. là optional chaining, giúp truy cập thuộc tính an toàn khi thuộc tính trước nó có thể không tồn tại.
Ví dụ:
data.errors?.titleNếu data.errors không tồn tại, JavaScript trả về undefined thay vì phát sinh lỗi khi truy cập title.
Bây giờ chúng ta xây dựng một trang HTML nhỏ để tải danh sách bài viết từ API và hiển thị kết quả.
Ví dụ dưới đây sử dụng endpoint /api/posts. Endpoint cần tồn tại và trả về một mảng JSON có các thuộc tính id và title.
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Danh sách bài viết</title>
</head>
<body>
<h1>Danh sách bài viết</h1>
<button id="loadPosts">Tải bài viết</button>
<p id="message" role="status"></p>
<ul id="posts"></ul>
<script>
const button = document.querySelector("#loadPosts");
const postsElement = document.querySelector("#posts");
const messageElement = document.querySelector("#message");
button.addEventListener("click", async () => {
postsElement.replaceChildren();
messageElement.textContent = "Đang tải dữ liệu...";
try {
const response = await fetch("/api/posts", {
headers: {
Accept: "application/json"
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const posts = await response.json();
if (!Array.isArray(posts)) {
throw new Error("API không trả về danh sách hợp lệ.");
}
for (const post of posts) {
const item = document.createElement("li");
item.textContent = `${post.id}: ${post.title}`;
postsElement.appendChild(item);
}
messageElement.textContent =
`Đã tải ${posts.length} bài viết.`;
} catch (error) {
messageElement.textContent =
`Không thể tải dữ liệu: ${error.message}`;
}
});
</script>
</body>
</html>
querySelector() tìm các phần tử HTML.
addEventListener() lắng nghe thao tác nhấn nút.
fetch() gọi API.
response.json() chuyển body JSON thành dữ liệu JavaScript.
Array.isArray() kiểm tra dữ liệu có phải mảng hay không.
createElement() và appendChild() tạo danh sách bài viết.
textContent hiển thị nội dung văn bản an toàn hơn so với đưa dữ liệu API trực tiếp vào innerHTML.
Nếu Laravel và frontend chạy trên các origin khác nhau, bạn cần cấu hình URL API chính xác và xử lý CORS phù hợp. Nếu sử dụng cookie để xác thực, cấu hình credentials và CSRF cũng sẽ cần được xem xét.
Bài 1: Chuyển JSON String thành JavaScript Object.
Xem đáp ánBài 2: Chuyển JavaScript Object thành JSON String.
Xem đáp ánBài 3: Lấy danh sách bài viết từ /api/posts.
Xem đáp ánBài 4: Gửi dữ liệu tạo bài viết bằng HTTP Method nào?
Xem đáp ánBài 5: Laravel trả status 422. Đây thường là lỗi gì?
Xem đáp ánBài 6: Vì sao phải kiểm tra response.ok?
Xem đáp ánSau bài học này, bạn cần nắm chắc những kiến thức sau:
JSON dùng để trao đổi dữ liệu giữa frontend và backend.
JSON.parse() chuyển JSON String thành giá trị JavaScript.
JSON.stringify() chuyển dữ liệu JavaScript thành JSON String.
HTTP Method xác định loại thao tác với tài nguyên.
HTTP Status Code cho biết kết quả xử lý request.
fetch() kết hợp response.json() giúp nhận dữ liệu từ API.
Khi gửi JSON, thường sử dụng Content-Type: application/json.
Luôn kiểm tra response.ok và xử lý các lỗi có thể xảy ra.
JavaScript gọi Laravel REST API
Ở bài tiếp theo, chúng ta sẽ đưa các kiến thức JSON, HTTP, Promise, Async/Await và Fetch API vào một tình huống thực tế:
Kết nối JavaScript với Laravel 13.
Gọi API lấy danh sách bài viết.
Hiển thị dữ liệu Laravel trên trình duyệt.
Gửi dữ liệu tạo bài viết mới.
Xử lý lỗi validation và HTTP Response.
Hoàn thiện luồng giao tiếp frontend–backend trước khi chuyển sang Vue.
Đây là bước chuyển từ học JavaScript riêng lẻ sang xây dựng ứng dụng frontend thực sự.
Gợi ý: Hãy thử chạy bài thực hành với một endpoint /api/posts của Laravel trước khi sang Bài 08; bạn sẽ dễ hiểu cách Vue kết nối backend hơn.
✦ Made by AI ✦
Cập nhật: 2026-10-27T06:25:13.984+07:00
Trong các bài trước, chúng ta đã học Promise và Async/Await.&
Bây giờ, chúng ta sẽ sử dụng những kiến thức đó để giao tiếp với máy chủ thông qua Fetch API.
fetch() là API có sẵn trong trình duyệt, cho phép JavaScript gửi HTTP request đến máy chủ và nhận dữ liệu trả về.
Ví dụ, 01 website có thể sử dụng Fetch API để:
Lấy danh sách bài viết từ Laravel.
Tìm kiếm bài viết mà không cần tải lại trang.
Gửi dữ liệu từ biểu mẫu đăng ký.
Lấy thông tin người dùng từ REST API.
Thêm, sửa, xóa dữ liệu thông qua API.
Hãy hình dung quy trình như sau:
Frontend — JavaScript
Gửi request bằng fetch()
Backend — Laravel 13
Xử lý request, truy vấn database
Database
Trả dữ liệu cho Laravel
Laravel trả response → JavaScript nhận dữ liệu → giao diện hiển thị kết quả.
Lưu ý: Fetch API không kết nối trực tiếp đến database. Nó giao tiếp với máy chủ thông qua HTTP.
Cú pháp đơn giản nhất:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error("Lỗi:", error);
});Giải thích từng phần:
Thành phần | Ý nghĩa |
|---|---|
| Gửi request đến URL |
| Đối tượng response nhận từ máy chủ |
| Đọc body và phân tích JSON thành dữ liệu JavaScript |
| Dữ liệu sau khi phân tích |
| Xử lý lỗi mạng hoặc lỗi trong chuỗi Promise |
Trong ví dụ trên, chúng ta dùng một API công khai để thực hành, chưa cần cài đặt Laravel.
Ở Bài 05, chúng ta đã học async/await.&
Đây là cách viết thường dễ đọc hơn khi xử lý nhiều bước bất đồng bộ.
async function getPosts() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
);
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Lỗi:", error);
}
}
getPosts();Phân tích:
async cho phép sử dụng await bên trong hàm.
await fetch(...) chờ request hoàn tất và nhận response.
await response.json() chờ đọc và phân tích nội dung JSON.
try/catch giúp xử lý những lỗi có thể xảy ra.
Điểm quan trọng: fetch() mặc định chỉ sử dụng phương thức GET nếu không khai báo phương thức khác.
Đây là ví dụ thực tế hơn: lấy danh sách bài viết và hiển thị chúng trên trang web.
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fetch API - Danh sách bài viết</title>
</head>
<body>
<h1>Danh sách bài viết</h1>
<button id="loadPosts">Tải bài viết</button>
<p id="status"></p>
<ul id="postList"></ul>
<script>
const button = document.getElementById("loadPosts");
const postList = document.getElementById("postList");
const status = document.getElementById("status");
button.addEventListener("click", async () => {
status.textContent = "Đang tải dữ liệu...";
postList.replaceChildren();
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=5"
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const posts = await response.json();
posts.forEach(post => {
const li = document.createElement("li");
const title = document.createElement("h3");
const body = document.createElement("p");
title.textContent = post.title;
body.textContent = post.body;
li.append(title, body);
postList.appendChild(li);
});
status.textContent = "Tải dữ liệu thành công!";
} catch (error) {
status.textContent = "Không thể tải dữ liệu.";
console.error(error);
}
});
</script>
</body>
</html>
Bạn có thể lưu thành file index.html và mở bằng trình duyệt có kết nối Internet để thử nghiệm.
Kết quả mong đợi:
Ban đầu chỉ có tiêu đề và nút tải bài viết.
Nhấn nút, trình duyệt gửi request đến API.
Năm bài viết được hiển thị trên giao diện.
Nếu xảy ra lỗi, trang hiển thị thông báo tương ứng.
Ở đây, chúng ta sử dụng textContent thay vì chèn trực tiếp dữ liệu bên ngoài bằng innerHTML.&
Đây là thói quen tốt để hạn chế rủi ro khi hiển thị dữ liệu không đáng tin cậy.
Đây là kiến thức rất quan trọng khi làm việc với Laravel REST API.
Nhiều người mới học thường viết:
const response = await fetch("/api/posts");
const data = await response.json();Cách viết này có thể hoạt động, nhưng vẫn còn thiếu một bước kiểm tra.
Vấn đề: fetch() thường không tự ném lỗi khi máy chủ trả về các mã HTTP như 404 Not Found hoặc 500 Internal Server Error.
Vì vậy, nên kiểm tra response.ok:
async function getPosts() {
try {
const response = await fetch("/api/posts");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const posts = await response.json();
console.log(posts);
} catch (error) {
console.error("Không thể lấy bài viết:", error);
}
}Cần phân biệt ba tình huống:
Tình huống | Cách xử lý |
|---|---|
Máy chủ trả | Kiểm tra và đọc dữ liệu |
Máy chủ trả | Kiểm tra |
Mất mạng, lỗi kết nối hoặc bị trình duyệt chặn bởi CORS | Xử lý ngoại lệ bằng |
Một lưu ý khác: nếu Laravel trả về JSON lỗi, chẳng hạn 422 Unprocessable Content, bạn có thể đọc phần JSON đó để hiển thị lỗi xác thực cho người dùng. Tuy nhiên, cần kiểm tra response.ok trước khi coi request là thành công.
Fetch API không chỉ dùng để lấy dữ liệu. Chúng ta còn có thể gửi dữ liệu đến backend bằng các phương thức như POST, PUT, PATCH và DELETE.
Ví dụ gửi một bài viết mới:
async function createPost() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
title: "Học Fetch API",
body: "Thực hành JavaScript với REST API",
userId: 1
})
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
console.log("Kết quả:", result);
} catch (error) {
console.error("Lỗi gửi dữ liệu:", error);
}
}
createPost();Giải thích:
method: "POST": khai báo phương thức gửi dữ liệu.
headers: mô tả định dạng dữ liệu gửi và loại response mong muốn.
JSON.stringify(): chuyển object JavaScript thành chuỗi JSON.
body: nội dung request gửi lên máy chủ.
API dùng trong ví dụ là API mô phỏng.&
Nó có thể trả về kết quả giả lập, không đồng nghĩa với việc dữ liệu đã được lưu bền vững vào database.
Khi áp dụng vào dự án Laravel, quy trình có thể như sau:
Vue / JavaScript
|
| fetch("/api/posts")
v
Laravel Route
|
v
Controller
|
v
Model / Database
|
v
JSON Response
|
v
JavaScript cập nhật giao diệnVí dụ, Laravel định nghĩa route:
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::get('/posts', [PostController::class, 'index']);Giả sử controller trả về danh sách bài viết dưới dạng JSON.&
Phía JavaScript có thể lấy dữ liệu bằng:
async function getPosts() {
try {
const response = await fetch("/api/posts", {
headers: {
"Accept": "application/json"
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const posts = await response.json();
console.log(posts);
} catch (error) {
console.error("Lỗi:", error);
}
}
getPosts();Đường dẫn /api/posts ở đây chỉ hoạt động khi Laravel đã được cấu hình route tương ứng và frontend có thể truy cập đúng máy chủ.&
Nếu frontend và backend ở hai origin khác nhau, cần cấu hình CORS phù hợp.&
Các API cần xác thực cũng phải sử dụng cơ chế xác thực tương ứng; chỉ biết URL API không đồng nghĩa với việc được phép truy cập mọi dữ liệu.
KIỂM TRA KIẾN THỨC · 4 CÂU
Câu 1. Fetch API thường được sử dụng để làm gì?
Câu 2. Hàm nào dùng để phân tích response JSON?
Câu 3. Vì sao cần kiểm tra response.ok?
Câu 4. Dùng gì để chuyển object thành chuỗi JSON khi gửi request?
Sau bài này, bạn cần nắm được:
fetch() dùng để gửi HTTP request từ JavaScript.
async/await giúp viết mã bất đồng bộ dễ đọc hơn.
response.json() đọc và phân tích dữ liệu JSON.
response.ok giúp kiểm tra kết quả HTTP.
JSON.stringify() chuyển object thành chuỗi JSON để gửi đi.
Fetch API là một trong những cầu nối giữa frontend và Laravel REST API.
Bài 07 — Xử lý JSON & HTTP sẽ đi sâu vào cấu trúc JSON, HTTP methods, status codes, request headers, response headers và cách xử lý lỗi API. Đây là nền tảng cần thiết trước khi sang Bài 08: JavaScript gọi Laravel REST API.
✦ Made by AI ✦
x1
Cập nhật: 2026-10-25T23:10:54.353+07:00
Đừng cộng theo cách thông thường nếu có cách làm ngắn hơn.
Ví dụ, gặp phép tính:
68 + 27 = ?
Cách thông thường: 8+7=15, nhớ 1, rồi cộng hàng chục.
Cách tính nhẩm nhanh hơn:
68 cần thêm 2 để thành 70.
Tách 27 thành 2+25.
Tính 70+25=95.
Vậy 68+27=95.
Hãy thử tính trong đầu trước khi xem đáp án. Bạn có thể bấm nút để kiểm tra từng câu.
Câu 1
✓ Chính xác!
48 cần thêm 2 để thành 50; 27 giảm 2 còn 25.
Câu 2
✓ Chính xác!
67 cần thêm 3 để thành 70; 18 giảm 3 còn 15.
Câu 3
✓ Chính xác!
96 cần thêm 4 để thành 100; 37 giảm 4 còn 33.
Câu 4
✓ Chính xác!
258 cần thêm 42 để thành 300; tách 37 thành 42 − 5 sẽ không tiện. Hãy tính 258 + 40 − 3.
Câu 5
✓ Chính xác!
Cộng 50 rồi bớt 1: 175 + 50 − 1.
Câu 6
✓ Chính xác!
Cộng 400 rồi bớt 1: 400 + 86 − 1.
Câu 7
✓ Chính xác!
Cộng 200 rồi bớt 2.
Câu 8
✓ Chính xác!
375 cần thêm 125 để đạt đúng 500.
Câu 9
✓ Chính xác!
Cộng 13 để thành 500, sau đó cộng 23.
Câu 10
✓ Chính xác!
Cộng 200 rồi bớt 3.
Mục tiêu của bài 1: Không cần thuộc lòng nhiều mẹo. Chỉ cần hình thành phản xạ nhìn thấy số gần 10, 100 hoặc 500 là biết cách bù tròn.
Sau khi bạn làm xong, ta sẽ chuyển sang Bài 2: Cộng nhẩm các số có hai chữ số nhanh hơn cách cộng thông thường, rồi tăng dần độ khó đến phép nhân ba chữ số.
✦ Made by AI ✦
Cập nhật: 2026-10-25T17:19:06.925+07:00
Bài | Nội dung |
|---|---|
1 | Cộng nhanh bằng cách bù tròn chục |
2 | Cộng hai số có hai chữ số |
3 | Cộng ba số liên tiếp trong một phép tính |
4 | Cộng số có ba chữ số |
5 | Nhân nhanh với 2, 3, 4, 5 |
6 | Nhân nhanh với 9, 11, 25, 50 |
7 | Nhân số có hai chữ số với một chữ số |
8 | Nhân hai số có hai chữ số |
9 | Nhân số gần 100 |
10 | Nhân bằng cách phân tích và phân phối |
11 | Kết hợp cộng và nhân |
12 | Thi tính nhẩm siêu tốc |
Mỗi bài sẽ gồm: mẹo tư duy → ví dụ giải thích → bài tập thực hành → thử thách tính giờ. Ta sẽ học từ dễ đến khó, không cần dùng phép trừ hay phép chia làm kỹ thuật chính.
Cập nhật: 2026-10-25T17:05:52.055+07:00
Trong JavaScript, có những công việc hoàn thành gần như ngay lập tức:
const name = 'Doraemon';
console.log(name);Nhưng cũng có những công việc cần thời gian chờ:
Gọi Laravel REST API.
Tải danh sách bài viết từ Database thông qua API.
Đọc dữ liệu từ máy chủ.
Tải hình ảnh.
Gửi Form đăng nhập.
Chờ kết quả từ một dịch vụ bên ngoài.
Ví dụ, khi ứng dụng cần lấy danh sách bài viết từ Laravel:
JavaScript
│
│ Gửi yêu cầu
▼
Laravel REST API
│
▼
Controller
│
▼
Database
│
│ Trả kết quả
▼
JavaScript nhận dữ liệuQuá trình này có thể mất một khoảng thời gian.
JavaScript cần cơ chế xử lý công việc bất đồng bộ mà không phải chặn toàn bộ hoạt động của trình duyệt trong lúc chờ.
Hai kiến thức quan trọng chúng ta cần học là:
Promise
async/await
Ví dụ:
console.log('Doraemon');
console.log('Nobita');
console.log('Pikachu');Kết quả:
Doraemon
Nobita
PikachuCác câu lệnh được thực thi theo thứ tự.
Ví dụ:
console.log('Doraemon');
setTimeout(() => {
console.log('Nobita');
}, 2000);
console.log('Pikachu');Kết quả:
Doraemon
Pikachu
NobitasetTimeout() lên lịch thực thi callback sau khoảng thời gian chỉ định.
Trong thời gian đó, JavaScript tiếp tục thực hiện các công việc khác.
Điều quan trọng cần hiểu là: bất đồng bộ không có nghĩa là JavaScript luôn chạy nhiều đoạn code cùng lúc.
JavaScript trong trình duyệt thường thực thi code trên một luồng chính, trong khi các thao tác như I/O có thể tiến hành mà không chặn luồng đó.
Callback là một hàm được truyền vào một hàm khác để được gọi sau đó.
Ví dụ:
function greet(name, callback) {
console.log(`Xin chào ${name}`);
callback();
}
greet('Doraemon', () => {
console.log('Bắt đầu học JavaScript');
});
Kết quả:
Xin chào Doraemon
Bắt đầu học JavaScript
Callback cũng xuất hiện trong các thao tác bất đồng bộ:
console.log('Bắt đầu');
setTimeout(() => {
console.log('Đã hoàn thành');
}, 1000);
console.log('Tiếp tục công việc');Callback có thể xử lý những trường hợp đơn giản.
Tuy nhiên, khi nhiều công việc phụ thuộc lẫn nhau, code có thể trở nên khó đọc nếu lồng nhiều callback.
Đây là một trong những lý do Promise ra đời.
Promise là một đối tượng đại diện cho kết quả của một công việc bất đồng bộ, có thể hoàn thành thành công hoặc thất bại.
Một Promise có ba trạng thái:
|
Pending Đang chờ kết quả |
|
|
✅ Fulfilled Hoàn thành thành công |
❌ Rejected Thất bại |
Ví dụ tưởng tượng:
Doraemon gửi yêu cầu lấy bánh
│
▼
Pending
│
┌─────┴─────┐
▼ ▼
Có bánh Hết bánh
│ │
▼ ▼
Fulfilled RejectedĐây chỉ là ví dụ minh họa.
Trong ứng dụng thật, Promise có thể đại diện cho 01 yêu cầu API, 01 thao tác đọc file hoặc 01 công việc bất đồng bộ khác.
Chúng ta có thể tự tạo Promise bằng new Promise().
const promise = new Promise((resolve, reject) => {
const hasCake = true;
if (hasCake) {
resolve('Doraemon đã nhận được bánh!');
} else {
reject(new Error('Đã hết bánh!'));
}
});Hai hàm quan trọng:
resolve(value): hoàn thành Promise thành công.
reject(error): hoàn thành Promise với trạng thái thất bại.
Để nhận kết quả, chúng ta sử dụng .then() và .catch().
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
});Kết quả:
Doraemon đã nhận được bánh!Lưu ý:
pending sang trạng thái cuối cùng một lần..then() — nhận kết quả thành côngVí dụ:
const getScore = new Promise(resolve => {
resolve(100);
});
getScore.then(score => {
console.log(`Điểm của Pikachu: ${score}`);
});
Kết quả:
Điểm của Pikachu: 100.then() nhận kết quả khi Promise hoàn thành thành công.
Có thể nối nhiều bước xử lý:
Promise.resolve(10)
.then(number => number * 2)
.then(number => number + 5)
.then(result => {
console.log(result);
});Kết quả:
25Mỗi .then() có thể trả về một giá trị hoặc một Promise.
Giá trị đó sẽ được chuyển sang bước tiếp theo.
Đây là cơ chế Promise chaining — nối chuỗi Promise.
.catch() — xử lý lỗiVí dụ:
const promise = new Promise((resolve, reject) => {
reject(new Error('Conan không tìm thấy dữ liệu!'));
});
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
});Kết quả:
Conan không tìm thấy dữ liệu!.catch() xử lý trường hợp Promise bị reject hoặc lỗi phát sinh trong các bước xử lý trước đó của chuỗi Promise.
Trong ứng dụng Laravel, lỗi có thể xuất phát từ:
API không truy cập được.
Server trả về lỗi.
Dữ liệu không đúng định dạng mong đợi.
Code xử lý dữ liệu phát sinh exception.
Tuy nhiên, một điểm quan trọng: fetch() không tự reject Promise chỉ vì máy chủ trả về HTTP 404 hoặc 500.
Chúng ta sẽ xử lý trường hợp đó ở Bài 06.
.finally() — luôn thực hiện bước kết thúcGiả sử trong lúc tải dữ liệu, giao diện hiển thị:
Đang tải dữ liệu...Khi công việc kết thúc, dù thành công hay thất bại, chúng ta muốn ẩn thông báo tải.
Có thể dùng .finally():
Promise.resolve('Dữ liệu của Doraemon')
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
})
.finally(() => {
console.log('Kết thúc quá trình xử lý');
});.finally() thường được dùng để:
Ẩn trạng thái Loading.
Bật lại nút Submit.
Dọn dẹp tài nguyên.
Thực hiện công việc kết thúc.
Giá trị trả về từ .finally() thông thường không thay thế kết quả của Promise trước đó, trừ khi callback trong .finally() phát sinh lỗi hoặc trả về Promise bị reject.
setTimeout()Tạo 01 hàm giả lập việc tải dữ liệu từ máy chủ:
function getCharacter() {
return new Promise(resolve => {
setTimeout(() => {
resolve({
name: 'Doraemon',
power: 100
});
}, 2000);
});
}
getCharacter().then(character => {
console.log(character);
});Sau khoảng hai giây, Console hiển thị:
{
name: 'Doraemon',
power: 100
}Hàm getCharacter() trả về một Promise ngay lập tức.
Giá trị dữ liệu chỉ được cung cấp khi Promise hoàn thành.
Đây là mô hình gần giống việc chờ một API trả dữ liệu.
async/await là cú pháp giúp viết code sử dụng Promise theo cách dễ đọc hơn.
Thay vì viết:
getCharacter()
.then(character => {
console.log(character.name);
})
.catch(error => {
console.error(error.message);
});Chúng ta có thể viết:
async function showCharacter() {
try {
const character = await getCharacter();
console.log(character.name);
} catch (error) {
console.error(error.message);
}
}
showCharacter();Kết quả vẫn là:
DoraemonSự khác biệt chủ yếu nằm ở cách tổ chức code.
async/await không thay thế Promise.
Nó sử dụng Promise và giúp chúng ta viết các bước bất đồng bộ dễ theo dõi hơn.
asyncKhi khai báo một hàm bằng async, hàm đó luôn trả về một Promise.
Ví dụ:
async function sayHello() {
return 'Xin chào Doraemon!';
}
sayHello().then(message => {
console.log(message);
});Kết quả:
Xin chào Doraemon!Ngay cả khi chúng ta trả về một chuỗi thông thường, JavaScript cũng bọc kết quả đó trong Promise đã hoàn thành thành công.
Có thể viết Arrow Function với async:
const sayHello = async () => {
return 'Xin chào Pikachu!';
};
sayHello().then(console.log);Cú pháp này sẽ xuất hiện thường xuyên khi viết Vue và React.
await dùng để chờ một Promise hoàn thành và lấy kết quả của nó bên trong hàm async.
Ví dụ:
function getPower() {
return new Promise(resolve => {
setTimeout(() => {
resolve(100);
}, 1000);
});
}
async function showPower() {
console.log('Đang kiểm tra sức mạnh...');
const power = await getPower();
console.log(`Sức mạnh của Pikachu: ${power}`);
}
showPower();
console.log('JavaScript vẫn tiếp tục thực hiện công việc khác');Thứ tự kết quả:
Đang kiểm tra sức mạnh...
JavaScript vẫn tiếp tục thực hiện công việc khác
Sức mạnh của Pikachu: 100Khi chạy đến await, hàm showPower() tạm dừng tại đó để chờ Promise, nhưng luồng JavaScript vẫn có thể thực hiện những công việc khác.
Lưu ý:
await thường được sử dụng bên trong hàm async.await trong ES Modules ở những môi trường tương thích, nhưng trong bài này chúng ta sử dụng await bên trong hàm để dễ tổ chức code.try/catchKhi sử dụng await, chúng ta có thể dùng try/catch để xử lý lỗi.
function getCake() {
return new Promise((resolve, reject) => {
reject(new Error('Không còn bánh cho Nobita!'));
});
}
async function receiveCake() {
try {
const cake = await getCake();
console.log(cake);
} catch (error) {
console.error(error.message);
}
}
receiveCake();Kết quả:
Không còn bánh cho Nobita!Cách này giúp chúng ta tổ chức code rõ ràng hơn khi có nhiều thao tác bất đồng bộ.
Ví dụ với Laravel API:
async function loadPosts() {
try {
// Gọi API tại đây.
} catch (error) {
// Xử lý lỗi tại đây.
}
}Ở Bài 06, chúng ta sẽ thay phần giả lập bằng fetch() để gọi API thật.
| Tiêu chí | Promise chaining | Async/Await |
|---|---|---|
| Nhận kết quả | .then() | await |
| Xử lý lỗi | .catch() | try/catch |
| Dọn dẹp cuối quá trình | .finally() | Có thể dùng finally trong try/catch/finally |
| Phù hợp | Chuỗi xử lý ngắn | Quy trình nhiều bước |
| Bản chất | Promise | Vẫn sử dụng Promise |
02 cách đều hợp lệ.
Ví dụ Promise chaining:
getCharacter()
.then(character => character.name)
.then(name => console.log(name))
.catch(console.error);Ví dụ Async/Await:
async function showName() {
try {
const character = await getCharacter();
console.log(character.name);
} catch (error) {
console.error(error);
}
}
showName();Trong các bài tiếp theo, chúng ta sẽ ưu tiên async/await cho các ví dụ gọi API vì cách viết này dễ theo dõi khi có nhiều bước xử lý.
Promise.all()Giả sử cần lấy thông tin của ba nhân vật.
function getCharacter(name, delay) {
return new Promise(resolve => {
setTimeout(() => {
resolve(name);
}, delay);
});
}
async function loadCharacters() {
const characters = await Promise.all([
getCharacter('Doraemon', 2000),
getCharacter('Pikachu', 1000),
getCharacter('Conan', 1500)
]);
console.log(characters);
}
loadCharacters();Kết quả:
['Doraemon', 'Pikachu', 'Conan']Thứ tự kết quả vẫn theo thứ tự các Promise được truyền vào Promise.all(), không phụ thuộc vào thứ tự hoàn thành.
Trong ví dụ này, ba công việc được bắt đầu gần như cùng lúc, nên tổng thời gian chờ thường xấp xỉ công việc lâu nhất thay vì tổng thời gian của cả ba.
Promise.all()?Khi cần thực hiện nhiều công việc độc lập, chẳng hạn:
Tải danh sách bài viết.
Tải danh mục.
Tải thông tin thống kê.
Nếu một Promise bị reject, Promise tổng hợp từ Promise.all() cũng reject.
Nếu cần nhận kết quả thành công và thất bại riêng của từng công việc, chúng ta có thể tìm hiểu Promise.allSettled() ở phần mở rộng sau này.
Hai cách viết dưới đây có ý nghĩa khác nhau.
async function loadData() {
const a = await getCharacter('Doraemon', 1000);
const b = await getCharacter('Pikachu', 1000);
console.log(a, b);
}Công việc thứ hai chỉ bắt đầu sau khi công việc thứ nhất hoàn thành.
async function loadData() {
const promiseA = getCharacter('Doraemon', 1000);
const promiseB = getCharacter('Pikachu', 1000);
const [a, b] = await Promise.all([
promiseA,
promiseB
]);
console.log(a, b);
}Hai Promise được tạo trước khi await chờ kết quả.
Vì vậy, các công việc có thể tiến hành đồng thời.
Quy tắc:
Chúng ta sẽ tạo một trang đơn giản mô phỏng việc tải danh sách bài viết.
Ở bài này, dữ liệu vẫn là dữ liệu giả lập. Chúng ta chưa cần Laravel API thật.
resources/js/modules/posts.jsexport function getPosts() {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
id: 1,
title: 'Laravel 13',
author: 'Doraemon'
},
{
id: 2,
title: 'JavaScript hiện đại',
author: 'Nobita'
},
{
id: 3,
title: 'Vue cơ bản',
author: 'Pikachu'
}
]);
}, 1500);
});
}Hàm getPosts() trả về một Promise, mô phỏng API mất khoảng 1,5 giây để trả dữ liệu.
resources/js/app.jsimport { getPosts } from './modules/posts.js';
const postList = document.querySelector('#postList');
const loading = document.querySelector('#loading');
const errorMessage = document.querySelector('#errorMessage');
async function loadPosts() {
try {
if (loading) {
loading.hidden = false;
}
if (errorMessage) {
errorMessage.textContent = '';
}
const posts = await getPosts();
if (!postList) {
return;
}
postList.replaceChildren();
posts.forEach(post => {
const li = document.createElement('li');
const title = document.createElement('strong');
const author = document.createElement('span');
title.textContent = post.title;
author.textContent = ` — ${post.author}`;
li.append(title, author);
postList.append(li);
});
} catch (error) {
if (errorMessage) {
errorMessage.textContent = error.message;
}
} finally {
if (loading) {
loading.hidden = true;
}
}
}
loadPosts();Trong ví dụ này:
async khai báo hàm bất đồng bộ.
await chờ dữ liệu.
try/catch xử lý lỗi.
finally ẩn trạng thái Loading sau khi quá trình kết thúc.
Tạo file resources/views/javascript.blade.php:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promise & Async/Await</title>
@vite(['resources/js/app.js'])
</head>
<body>
<h1>Thư viện bài học</h1>
<p id="loading" hidden>
Đang tải dữ liệu...
</p>
<p id="errorMessage" role="alert"></p>
<ul id="postList"></ul>
</body>
</html>Nếu đã có trang /javascript từ những bài trước, hãy dùng lại trang đó hoặc tạo một view và Route mới để tránh khai báo Route trùng.
Trong terminal thứ nhất:
npm run devTrong terminal thứ hai:
php artisan serveMở trang Laravel tương ứng trong trình duyệt.
Bạn sẽ thấy thông báo “Đang tải dữ liệu...” xuất hiện trong khoảng 1,5 giây.
Sau đó, danh sách ba bài viết sẽ hiện ra.
Đây là cách mô phỏng trạng thái Loading mà chúng ta thường gặp khi chờ dữ liệu từ API.
awaitasync function loadPosts() {
const posts = getPosts();
console.log(posts);
}Biến posts lúc này chứa một Promise, chưa phải danh sách bài viết.
Cách đúng:
async function loadPosts() {
const posts = await getPosts();
console.log(posts);
}await nhưng không xử lý lỗiasync function loadPosts() {
const posts = await getPosts();
console.log(posts);
}Nếu Promise bị reject, lỗi có thể trở thành một rejected Promise của hàm loadPosts() mà không được xử lý tại đây.
Nên chủ động dùng try/catch khi cần xử lý lỗi tại chỗ.
fetch() trả lỗi khi HTTP 404fetch() thường vẫn hoàn thành thành công ở cấp độ Promise khi nhận được phản hồi HTTP 404 hoặc 500.
Muốn phát hiện lỗi HTTP, chúng ta cần kiểm tra thuộc tính response.ok.
Nội dung này sẽ được thực hành trong Bài 06 — Fetch API.
await bên trong forEach()Ví dụ không nên dùng khi muốn chờ toàn bộ công việc hoàn thành:
posts.forEach(async post => {
await savePost(post);
});forEach() không chờ các Promise do callback async trả về.
Nếu cần chạy tuần tự, có thể dùng vòng lặp for...of:
for (const post of posts) {
await savePost(post);
}Nếu các công việc độc lập và có thể chạy song song:
await Promise.all(posts.map(post => savePost(post)));Chỉ sử dụng cách thứ hai nếu hàm savePost() trả về Promise và việc chạy các thao tác song song là an toàn đối với ứng dụng.
Tạo Promise trả về chuỗi:
Doraemon đã hoàn thành nhiệm vụ!Sau đó dùng .then() để in kết quả.
Tạo Promise bị reject với thông báo:
Pikachu không tìm thấy năng lượng!Dùng .catch() để hiển thị lỗi.
Viết hàm getCharacter() trả về Promise chứa thông tin:
{
name: 'Conan',
level: 10
}Viết hàm showCharacter() sử dụng async/await để hiển thị tên và cấp độ.
Dựa trên ví dụ Laravel ở phần 17, bổ sung một nút “Tải lại”.
Khi bấm nút, danh sách được tải lại và trạng thái Loading được hiển thị trong lúc chờ.
Tạo ba Promise trả về tên Doraemon, Nobita và Pikachu sau những khoảng thời gian khác nhau.
Dùng Promise.all() để nhận kết quả và in ra Console.
| Kiến thức | Công dụng |
|---|---|
| Callback | Truyền hàm để thực hiện sau |
Promise | Đại diện cho kết quả của công việc bất đồng bộ |
.then() | Xử lý kết quả thành công |
.catch() | Xử lý lỗi |
.finally() | Thực hiện bước kết thúc |
async | Khai báo hàm trả về Promise |
await | Chờ Promise hoàn thành trong hàm bất đồng bộ |
try/catch | Xử lý lỗi theo cấu trúc |
Promise.all() | Chờ nhiều Promise độc lập cùng hoàn thành |
Điều cần nhớ nhất:
Promise giúp xử lý các công việc bất đồng bộ.
async/await là cách viết thuận tiện hơn khi làm việc với Promise.
try/catch giúp xử lý lỗi khi chờ Promise bằng await.
Promise.all() phù hợp khi cần chờ nhiều công việc độc lập.
Khi gọi API thật, cần phân biệt lỗi mạng với lỗi HTTP.
Bài 06 — Fetch API: chúng ta sẽ dùng fetch() để gửi HTTP request, nhận JSON từ Laravel REST API, hiển thị Loading và xử lý lỗi thực tế.
x1
Cập nhật: 2026-10-25T22:41:14.925+07:00
Trong các bài trước, chúng ta thường viết JavaScript trong một file:
resources/
└── js/
└── app.jsCách này phù hợp với những ví dụ đơn giản.
Nhưng hãy tưởng tượng ứng dụng Laravel của chúng ta có các chức năng:
Quản lý bài viết.
Quản lý danh mục.
Tìm kiếm.
Đăng nhập.
Hiển thị thông báo.
Gọi Laravel REST API.
Nếu tất cả code đều nằm trong app.js, file này sẽ ngày càng dài và khó bảo trì.
Module cho phép chúng ta chia code thành nhiều file, mỗi file đảm nhiệm một nhiệm vụ riêng.
Ví dụ:
resources/
└── js/
├── app.js
├── modules/
│ ├── posts.js
│ ├── users.js
│ ├── search.js
│ └── notification.js
└── services/
└── api.jsMỗi file có thể xuất những thành phần cần sử dụng và nhập các thành phần từ file khác.
Hai từ khóa quan trọng nhất là:
export: xuất dữ liệu, hàm hoặc class để module khác sử dụng.
import: nhập dữ liệu, hàm hoặc class từ module khác.
Có hai cách xuất thường gặp:
Named Export.
Default Export.
Tạo file resources/js/modules/characters.js:
export const doraemon = {
name: 'Doraemon',
type: 'Robot mèo',
color: 'blue'
};
export const nobita = {
name: 'Nobita',
type: 'Học sinh',
color: 'yellow'
};
export function sayHello(name) {
return `Xin chào ${name}!`;
}Trong file này, chúng ta đã xuất ba thành phần:
Biến doraemon.
Biến nobita.
Hàm sayHello().
Các module khác có thể nhập những thành phần này.
Lưu ý: export ở đây là Named Export vì mỗi thành phần được xuất bằng tên của nó.
Một module cũng có thể xuất một thành phần mặc định.
Tạo file resources/js/modules/pikachu.js:
const pikachu = {
name: 'Pikachu',
type: 'Electric',
power: 100
};
export default pikachu;Ở đây, pikachu được xuất bằng export default.
Mỗi module chỉ có một default export, nhưng có thể có nhiều Named Export.
Tạo file resources/js/app.js:
import {
doraemon,
nobita,
sayHello
} from './modules/characters.js';
console.log(doraemon);
console.log(nobita);
console.log(sayHello('Nobita'));Kết quả:
{
name: 'Doraemon',
type: 'Robot mèo',
color: 'blue'
}
{
name: 'Nobita',
type: 'Học sinh',
color: 'yellow'
}
Xin chào Nobita!Cú pháp:
import { tenThanhPhan } from './duong-dan-module.js';Tên được import phải khớp với tên Named Export.
Ví dụ, nếu module xuất doraemon, chúng ta không thể import trực tiếp bằng tên robotMeo nếu chưa đổi tên khi import.
Để nhập Pikachu:
import pikachu from './modules/pikachu.js';
console.log(pikachu.name);
console.log(pikachu.power);Kết quả:
Pikachu
100Điểm khác biệt: Default Export có thể được nhập bằng một tên khác.
Ví dụ:
import pokemon from './modules/pikachu.js';
console.log(pokemon.name);Code vẫn hoạt động vì pokemon chỉ là tên biến chúng ta chọn khi import Default Export.
| Đặc điểm | Named Export | Default Export |
|---|---|---|
| Số lượng trong một module | Có thể nhiều | Tối đa một |
| Khi import | Thường dùng {} | Không cần {} |
| Tên khi import | Theo tên export hoặc đổi tên rõ ràng | Có thể tự đặt |
| Phù hợp | Nhiều hàm, biến liên quan | Một thành phần chính |
Ví dụ kết hợp cả hai cách.
File resources/js/modules/pokemon.js:
export const pokemonType = 'Electric';
export function attack() {
return 'Thunderbolt!';
}
export default {
name: 'Pikachu',
power: 100
};File resources/js/app.js:
import pikachu, {
pokemonType,
attack
} from './modules/pokemon.js';
console.log(pikachu.name);
console.log(pokemonType);
console.log(attack());Kết quả:
Pikachu
Electric
Thunderbolt!Chúng ta có thể import Default Export và Named Export trong cùng một câu lệnh.
Giả sử module xuất hàm sayHello():
export function sayHello(name) {
return `Xin chào ${name}!`;
}Khi import, chúng ta có thể đổi tên:
import {
sayHello as greeting
} from './modules/characters.js';
console.log(greeting('Doraemon'));Kết quả:
Xin chào Doraemon!Từ khóa as cho phép đổi tên thành phần khi import.
Cách này hữu ích khi hai module có các thành phần trùng tên hoặc khi muốn tên gọi rõ nghĩa hơn trong file đang sử dụng.
*Giả sử file characters.js có nhiều Named Export:
export const doraemon = {
name: 'Doraemon'
};
export const nobita = {
name: 'Nobita'
};
export const shizuka = {
name: 'Shizuka'
};Chúng ta có thể nhập tất cả Named Export:
import * as characters from './modules/characters.js';
console.log(characters.doraemon.name);
console.log(characters.nobita.name);
console.log(characters.shizuka.name);Kết quả:
Doraemon
Nobita
Shizukacharacters là một namespace chứa các Named Export của module.
Lưu ý: cú pháp import * as ... không nhập Default Export như một thuộc tính mặc định của namespace theo cách này; nếu cần, hãy import Default Export riêng.
Module không chỉ xuất biến đơn giản.
File resources/js/modules/math.js:
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}Sử dụng:
import { add, multiply } from './modules/math.js';
console.log(add(10, 20));
console.log(multiply(10, 20));File resources/js/modules/characters.js:
export const characters = [
'Doraemon',
'Nobita',
'Shizuka',
'Pikachu',
'Conan'
];Sử dụng:
import { characters } from './modules/characters.js';
characters.forEach(character => {
console.log(character);
});File resources/js/modules/Character.js:
export default class Character {
constructor(name, power) {
this.name = name;
this.power = power;
}
introduce() {
return `${this.name} có sức mạnh ${this.power}`;
}
}Sử dụng:
import Character from './modules/characters.js';
const pikachu = new Character('Pikachu', 100);
console.log(pikachu.introduce());
Kết quả:
Pikachu có sức mạnh 100Đến đây, chúng ta sẽ áp dụng Module vào cấu trúc Laravel thực tế.
Thay vì để toàn bộ code trong app.js, hãy chia thành các file theo nhiệm vụ.
resources/
└── js/
├── app.js
├── modules/
│ ├── characters.js
│ ├── posts.js
│ └── search.js
└── services/
└── api.jsÝ nghĩa:
app.js: điểm khởi đầu của JavaScript.
characters.js: dữ liệu minh họa nhân vật.
posts.js: xử lý danh sách bài viết.
search.js: chức năng tìm kiếm.
api.js: nơi tổ chức những hàm gọi API.
Trong ứng dụng thực tế, không nhất thiết phải tạo đủ tất cả file. Chỉ nên tách module khi có chức năng cần tái sử dụng hoặc cần quản lý độc lập.
Tạo file resources/js/modules/posts.js:
export const posts = [
{
id: 1,
title: 'Laravel 13',
author: 'Doraemon',
views: 120
},
{
id: 2,
title: 'JavaScript ES6+',
author: 'Nobita',
views: 80
},
{
id: 3,
title: 'Vue cơ bản',
author: 'Pikachu',
views: 200
}
];
export function getPostById(id) {
return posts.find(post => post.id === id);
}
export function getTotalViews() {
return posts.reduce(
(total, post) => total + post.views,
0
);
}Module này xuất:
posts: danh sách bài viết.
getPostById(): tìm bài viết theo ID.
getTotalViews(): tính tổng lượt xem.
Tạo file resources/js/modules/search.js:
export function searchPosts(posts, keyword) {
const normalizedKeyword = keyword.trim().toLowerCase();
return posts.filter(post =>
post.title.toLowerCase().includes(normalizedKeyword)
);
}Hàm tìm kiếm nhận danh sách và từ khóa, sau đó trả về danh sách phù hợp.
Hàm không phụ thuộc vào một Array cố định nên có thể tái sử dụng ở nhiều nơi.
app.jsMở resources/js/app.js:
import {
posts,
getPostById,
getTotalViews
} from './modules/posts.js';
import {
searchPosts
} from './modules/search.js';
console.log('Danh sách bài viết:', posts);
console.log('Bài viết số 2:', getPostById(2));
console.log('Tổng lượt xem:', getTotalViews());
console.log('Kết quả tìm kiếm:', searchPosts(posts, 'javascript'));Khi Laravel nạp app.js thông qua Vite, các module được import sẽ được xử lý theo quan hệ phụ thuộc.
Chúng ta không cần đưa từng file module vào Blade bằng một thẻ script riêng.
Bây giờ, hãy xây dựng một ví dụ hoàn chỉnh.
posts.jsĐường dẫn:
resources/js/modules/posts.js
export const posts = [
{ id: 1, title: 'Laravel 13', author: 'Doraemon', views: 120 },
{ id: 2, title: 'JavaScript hiện đại', author: 'Nobita', views: 80 },
{ id: 3, title: 'Vue cơ bản', author: 'Shizuka', views: 200 },
{ id: 4, title: 'React cơ bản', author: 'Pikachu', views: 150 },
{ id: 5, title: 'REST API', author: 'Conan', views: 90 }
];
export function getTotalViews(items) {
return items.reduce((total, post) => total + post.views, 0);
}search.jsĐường dẫn:
resources/js/modules/search.js
export function searchPosts(posts, keyword) {
const query = keyword.trim().toLowerCase();
return posts.filter(post => {
const title = post.title.toLowerCase();
const author = post.author.toLowerCase();
return title.includes(query) || author.includes(query);
});
}Hàm tìm kiếm theo cả tiêu đề và tên nhân vật.
app.jsĐường dẫn:
resources/js/app.js
import { posts, getTotalViews } from './modules/posts.js';
import { searchPosts } from './modules/search.js';
const searchInput = document.querySelector('#searchInput');
const postList = document.querySelector('#postList');
const totalViews = document.querySelector('#totalViews');
function renderPosts(items) {
if (!postList) {
return;
}
postList.replaceChildren();
if (items.length === 0) {
const li = document.createElement('li');
li.textContent = 'Không tìm thấy bài viết.';
postList.append(li);
return;
}
items.forEach(post => {
const li = document.createElement('li');
const title = document.createElement('strong');
const author = document.createElement('span');
title.textContent = post.title;
author.textContent = ` — Tác giả: ${post.author} — ${post.views} lượt xem`;
li.append(title, author);
postList.append(li);
});
}
function updateTotalViews(items) {
if (totalViews) {
totalViews.textContent = getTotalViews(items);
}
}
function handleSearch() {
const keyword = searchInput?.value ?? '';
const filteredPosts = searchPosts(posts, keyword);
renderPosts(filteredPosts);
updateTotalViews(filteredPosts);
}
if (searchInput) {
searchInput.addEventListener('input', handleSearch);
}
renderPosts(posts);
updateTotalViews(posts);Tạo file:
resources/views/javascript.blade.php
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript Modules - Laravel 13</title>
@vite(['resources/js/app.js'])
</head>
<body>
<h1>Thư viện bài học</h1>
<label for="searchInput">Tìm kiếm:</label>
<input
type="search"
id="searchInput"
placeholder="Nhập tên bài học hoặc nhân vật..."
>
<h2>Danh sách bài viết</h2>
<ul id="postList"></ul>
<p>
Tổng lượt xem:
<strong id="totalViews">0</strong>
</p>
</body>
</html>Trong routes/web.php:
use Illuminate\Support\Facades\Route;
Route::view('/javascript', 'javascript');Nếu đã có Route /javascript từ Bài 02 hoặc Bài 03, hãy sử dụng lại Route đó, không khai báo trùng.
Mở terminal thứ nhất:
npm run devMở terminal thứ hai:
php artisan serveTruy cập:
http://127.0.0.1:8000/javascript
Thử nhập:
Laravel
Doraemon
Vue
Pikachu
Danh sách và tổng lượt xem sẽ được cập nhật theo kết quả tìm kiếm.
Điều quan trọng: Blade chỉ cần nạp app.js.&
Các module còn lại được app.js import.&
Đây là cách tổ chức JavaScript mà chúng ta sẽ tiếp tục sử dụng trong những ứng dụng Laravel lớn hơn.
Trong Laravel, Vite giúp xử lý các file JavaScript và quan hệ import giữa chúng.
javascript.blade.php
│
│ @vite()
▼
app.js
│
├── posts.js
│
└── search.js
│
▼
JavaScript chạy
trong trình duyệtTrong chế độ phát triển, Vite hỗ trợ cập nhật nhanh khi chúng ta chỉnh sửa code.&
Khi build production, Vite xử lý các module để tạo tài nguyên frontend phục vụ triển khai.
Không cần tự nối nội dung tất cả file JavaScript vào một file bằng tay.
Có. JavaScript hiện đại hỗ trợ ES Modules với type="module":
<script type="module" src="/js/app.js"></script>Tuy nhiên, trong project Laravel sử dụng Vite, hãy ưu tiên:
@vite(['resources/js/app.js'])Vite sẽ đảm nhiệm việc phục vụ và build tài nguyên theo cấu hình của dự án.
import { posts } from './post.js';Nhưng file thực tế là posts.js.
Hãy kiểm tra đúng tên file và đường dẫn tương đối tính từ file đang import.
Nếu module xuất:
export const doraemon = {
name: 'Doraemon'
};Thì cách import đúng là:
import { doraemon } from './modules/characters.js';import { Doraemon } from './modules/characters.js';Nếu tên export là doraemon, đoạn trên không đúng vì JavaScript phân biệt chữ hoa và chữ thường.
Ví dụ:
posts.js → search.js
search.js → posts.jsĐây là quan hệ phụ thuộc vòng.&
Một số trường hợp vẫn hoạt động, nhưng có thể gây lỗi khởi tạo hoặc làm code khó hiểu.&
Nên thiết kế module theo hướng mỗi module có nhiệm vụ rõ ràng, tránh phụ thuộc vòng không cần thiết.
Nếu file khác không xuất thành phần mà bạn đang import, JavaScript sẽ báo lỗi module.
Hãy kiểm tra tên và loại export trong file nguồn.
Tạo file characters.js và xuất ba nhân vật:
Doraemon
Nobita
Pikachu
Mỗi nhân vật có name và power.
Trong app.js, import danh sách nhân vật và in tên từng nhân vật ra Console.
Tạo file math.js, xuất hai hàm:
add(a, b)
multiply(a, b)
Import vào app.js và kiểm tra kết quả.
Tách hàm tìm kiếm bài viết khỏi app.js thành search.js.&
Sau đó import lại và đảm bảo chức năng vẫn hoạt động.
Tạo cấu trúc:
resources/js/
├── app.js
├── modules/
│ ├── characters.js
│ └── posts.js
└── services/
└── api.jsChưa cần gọi API thật.&
Hãy tập trung vào việc xuất và nhập dữ liệu giữa các file.
Trong bài này, chúng ta đã học:
| Kiến thức | Công dụng |
|---|---|
export | Xuất thành phần từ module |
export default | Xuất thành phần mặc định |
import | Nhập thành phần từ module khác |
as | Đổi tên khi import/export |
import * as | Gom Named Export vào namespace |
| ES Modules | Chia code thành nhiều file |
| Vite | Hỗ trợ phát triển và build frontend trong Laravel |
Ghi nhớ: app.js không nhất thiết phải chứa toàn bộ logic của ứng dụng. Nó có thể đóng vai trò điểm khởi đầu, nhập các module cần thiết rồi khởi tạo chức năng.
Khi chuyển sang Vue và React, chúng ta vẫn sử dụng import và export để tổ chức Component, hàm xử lý dữ liệu và các module dùng chung.
Bài 05 — Promise & Async/Await: học cách xử lý các công việc bất đồng bộ, chờ kết quả API và quản lý lỗi khi giao tiếp giữa JavaScript với Laravel.
x1
Cập nhật: 2026-10-18T21:05:19.958+07:00
Trong ứng dụng web, dữ liệu thường được tổ chức dưới hai dạng phổ biến: Array và Object.
Array dùng để lưu một danh sách dữ liệu.
const languages = [
'PHP',
'JavaScript',
'Python',
'Java'
];
console.log(languages[0]);
console.log(languages[1]);Kết quả:
PHP
JavaScriptArray sử dụng chỉ số bắt đầu từ 0.
Object dùng để lưu dữ liệu theo cặp khóa và giá trị.
const user = {
id: 1,
name: 'Pi',
email: 'pi@example.com',
role: 'user'
};
console.log(user.name);
console.log(user.email);Kết quả:
Pi
pi@example.comTrong thực tế, một danh sách thường chứa nhiều Object:
const users = [
{ id: 1, name: 'Pi', role: 'admin' },
{ id: 2, name: 'Hùng', role: 'user' },
{ id: 3, name: 'Chi', role: 'user' }
];Đây là cấu trúc dữ liệu rất phổ biến khi Laravel trả về danh sách người dùng hoặc bài viết dưới dạng JSON.
const posts = [
'Laravel 13',
'JavaScript',
'Vue'
];Lấy phần tử theo chỉ số:
console.log(posts[0]);
console.log(posts[1]);
console.log(posts[2]);Kết quả:
Laravel 13
JavaScript
VueSử dụng thuộc tính length:
console.log(posts.length);Kết quả:
3push()posts.push('React');
console.log(posts);Kết quả:
[
'Laravel 13',
'JavaScript',
'Vue',
'React'
]push() thêm phần tử vào cuối Array và thay đổi Array ban đầu.
pop()posts.pop();
console.log(posts);pop() xóa và trả về phần tử cuối cùng.
includes()const frameworks = ['Vue', 'React', 'Angular'];
console.log(frameworks.includes('Vue'));
console.log(frameworks.includes('Laravel'));Kết quả:
true
falseincludes() hữu ích khi cần kiểm tra một danh sách có chứa một giá trị hay không.
forEach()Giả sử có danh sách bài viết:
const posts = [
{ id: 1, title: 'Laravel 13' },
{ id: 2, title: 'JavaScript' },
{ id: 3, title: 'Vue' }
];Duyệt bằng vòng lặp for:
for (let i = 0; i < posts.length; i++) {
console.log(posts[i].title);
}Có thể viết ngắn hơn bằng forEach():
posts.forEach(post => {
console.log(post.title);
});Kết quả:
Laravel 13
JavaScript
VueNếu cần chỉ số của phần tử:
posts.forEach((post, index) => {
console.log(index, post.title);
});Lưu ý: forEach() chủ yếu dùng để thực hiện một hành động với từng phần tử.&
Nó không tạo ra Array mới và không chờ các hàm async bên trong hoàn thành.
map() — biến đổi dữ liệumap() duyệt từng phần tử và trả về một Array mới chứa kết quả biến đổi.
Ví dụ:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(number => number * 2);
console.log(doubled);Kết quả:
[2, 4, 6, 8, 10]Array ban đầu không bị thay đổi:
console.log(numbers);Kết quả:
[1, 2, 3, 4, 5]const posts = [
{ id: 1, title: 'Laravel 13' },
{ id: 2, title: 'JavaScript' },
{ id: 3, title: 'Vue' }
];
const titles = posts.map(post => post.title);
console.log(titles);Kết quả:
['Laravel 13', 'JavaScript', 'Vue']const posts = [
{ id: 1, title: 'Laravel', views: 100 },
{ id: 2, title: 'Vue', views: 80 }
];
const formattedPosts = posts.map(post => ({
...post,
url: `/posts/${post.id}`
}));
console.log(formattedPosts);Kết quả sẽ là danh sách bài viết có thêm thuộc tính url.
Lưu ý dấu ngoặc đơn quanh Object trong Arrow Function: chúng giúp JavaScript hiểu rằng đây là Object được trả về, không phải phần thân hàm.
Ghi nhớ: dùng map() khi muốn biến đổi từng phần tử thành dữ liệu mới.
filter() — lọc dữ liệufilter() tạo một Array mới chỉ chứa những phần tử thỏa mãn điều kiện.
Ví dụ:
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers);Kết quả:
[2, 4, 6]const posts = [
{ id: 1, title: 'Laravel', status: 'published' },
{ id: 2, title: 'Vue', status: 'draft' },
{ id: 3, title: 'React', status: 'published' }
];
const publishedPosts = posts.filter(
post => post.status === 'published'
);
console.log(publishedPosts);Kết quả chứa bài viết Laravel và React.
const users = [
{ id: 1, name: 'Pi', role: 'admin' },
{ id: 2, name: 'Hùng', role: 'user' },
{ id: 3, name: 'Chi', role: 'admin' }
];
const admins = users.filter(user => user.role === 'admin');
console.log(admins);Ghi nhớ: filter() dùng để lọc danh sách, không xóa dữ liệu khỏi Array ban đầu.
Việc lọc ở JavaScript chỉ ảnh hưởng dữ liệu đang có trong trình duyệt. Nếu cần phân quyền hoặc bảo vệ dữ liệu, Laravel vẫn phải kiểm tra ở phía Server.
find() — tìm một phần tửfind() trả về phần tử đầu tiên thỏa mãn điều kiện. Nếu không tìm thấy, kết quả là undefined.
const users = [
{ id: 1, name: 'Pi' },
{ id: 2, name: 'Hùng' },
{ id: 3, name: 'Chi' }
];
const user = users.find(user => user.id === 2);
console.log(user);Kết quả:
{ id: 2, name: 'Hùng' }Nếu tìm ID không tồn tại:
const user = users.find(user => user.id === 99);
console.log(user);Kết quả:
undefinedconst posts = [
{ id: 10, title: 'Laravel' },
{ id: 20, title: 'JavaScript' },
{ id: 30, title: 'Vue' }
];
const post = posts.find(post => post.id === 20);
console.log(post?.title);Kết quả:
JavaScriptDấu ?. giúp tránh lỗi khi không tìm thấy bài viết.
Phân biệt:
find() trả về một phần tử đầu tiên phù hợp.
filter() trả về một Array chứa tất cả phần tử phù hợp.
some() và every() — kiểm tra điều kiệnsome()Trả về true nếu có ít nhất một phần tử thỏa mãn điều kiện.
const scores = [4, 6, 8, 9];
const hasExcellentScore = scores.some(score => score >= 9);
console.log(hasExcellentScore);Kết quả:
trueevery()Trả về true nếu tất cả phần tử đều thỏa mãn điều kiện.
const scores = [7, 8, 9, 10];
const allPassed = scores.every(score => score >= 5);
console.log(allPassed);Kết quả:
trueỨng dụng với danh sách bài viết:
const posts = [
{ title: 'Laravel', published: true },
{ title: 'Vue', published: true }
];
console.log(posts.some(post => !post.published));
console.log(posts.every(post => post.published));Kết quả:
false
trueLưu ý: gọi some() hoặc every() trên Array rỗng sẽ lần lượt trả về false và true.
reduce() — tổng hợp dữ liệureduce() duyệt Array và tích lũy kết quả thành một giá trị.
Ví dụ tính tổng:
const prices = [100, 200, 300];
const total = prices.reduce((sum, price) => {
return sum + price;
}, 0);
console.log(total);Kết quả:
600Trong đó:
sum: giá trị tích lũy.
price: phần tử đang xử lý.
0: giá trị khởi tạo của bộ tích lũy.
Có thể viết ngắn hơn:
const total = prices.reduce((sum, price) => sum + price, 0);const posts = [
{ title: 'Laravel', views: 100 },
{ title: 'Vue', views: 200 },
{ title: 'React', views: 150 }
];
const totalViews = posts.reduce(
(total, post) => total + post.views,
0
);
console.log(totalViews);Kết quả:
450const posts = [
{ status: 'published' },
{ status: 'draft' },
{ status: 'published' },
{ status: 'published' }
];
const statistics = posts.reduce((result, post) => {
result[post.status] = (result[post.status] ?? 0) + 1;
return result;
}, {});
console.log(statistics);Kết quả:
{
published: 3,
draft: 1
}reduce() rất mạnh, nhưng không cần sử dụng nó cho mọi việc. Nếu chỉ cần biến đổi danh sách, hãy dùng map(); nếu chỉ cần lọc, hãy dùng filter().
sort()sort() sắp xếp các phần tử và thay đổi Array ban đầu.
Ví dụ sắp xếp số tăng dần:
const numbers = [40, 5, 100, 20];
numbers.sort((a, b) => a - b);
console.log(numbers);Kết quả:
[5, 20, 40, 100]Nếu viết numbers.sort() mà không có hàm so sánh, các số sẽ được sắp xếp theo thứ tự chuỗi, nên có thể cho kết quả không như mong muốn.
const posts = [
{ title: 'Laravel', views: 100 },
{ title: 'Vue', views: 300 },
{ title: 'React', views: 200 }
];
const sortedPosts = [...posts].sort((a, b) => b.views - a.views);
console.log(sortedPosts);Kết quả: Vue, React, Laravel.
Ở đây, [...posts] tạo một bản sao nông trước khi sắp xếp để tránh thay đổi thứ tự của Array gốc.
const post = {
id: 1,
title: 'Laravel 13',
views: 100
};
console.log(post.title);
console.log(post['views']);
Cả hai cách đều hợp lệ.
const post = {
id: 1,
title: 'Laravel'
};
post.views = 100;
post.title = 'Laravel 13';
console.log(post);const user = {
name: 'Pi',
email: 'pi@example.com'
};
delete user.email;
console.log(user);delete xóa thuộc tính email khỏi Object hiện tại.
Nếu muốn giữ nguyên Object ban đầu, hãy tạo Object mới thay vì sửa trực tiếp.
const user = {
name: 'Pi',
role: 'admin'
};
console.log(Object.keys(user));
console.log(Object.values(user));
console.log(Object.entries(user));Kết quả:
['name', 'role']
['Pi', 'admin']
[['name', 'Pi'], ['role', 'admin']]
Những phương thức này hữu ích khi cần kiểm tra, duyệt hoặc chuyển đổi dữ liệu Object.
Giả sử Laravel trả về danh sách bài viết:
{
"data": [
{
"id": 1,
"title": "Laravel 13",
"status": "published",
"views": 120
},
{
"id": 2,
"title": "JavaScript",
"status": "draft",
"views": 50
},
{
"id": 3,
"title": "Vue",
"status": "published",
"views": 200
}
]
}
Khi dữ liệu đã được chuyển thành JavaScript Object, chúng ta có thể xử lý bằng các phương thức Array.
const responseData = {
data: [
{ id: 1, title: 'Laravel 13', status: 'published', views: 120 },
{ id: 2, title: 'JavaScript', status: 'draft', views: 50 },
{ id: 3, title: 'Vue', status: 'published', views: 200 }
]
};
const posts = responseData.data;
// Lấy danh sách tiêu đề.
const titles = posts.map(post => post.title);
// Lấy các bài đã xuất bản.
const publishedPosts = posts.filter(
post => post.status === 'published'
);
// Tìm bài viết có ID bằng 3.
const post = posts.find(post => post.id === 3);
// Tính tổng lượt xem.
const totalViews = posts.reduce(
(total, post) => total + post.views,
0
);
console.log(titles);
console.log(publishedPosts);
console.log(post);
console.log(totalViews);
Đây là những thao tác nền tảng khi xây dựng trang danh sách bài viết bằng JavaScript, Vue hoặc React.
Giả sử có danh sách:
const posts = [
{ id: 1, title: 'Laravel 13 cơ bản', category: 'Laravel' },
{ id: 2, title: 'JavaScript hiện đại', category: 'JavaScript' },
{ id: 3, title: 'Laravel REST API', category: 'Laravel' },
{ id: 4, title: 'Vue cơ bản', category: 'Vue' }
];
Tạo chức năng tìm kiếm:
const searchPosts = (posts, keyword) => {
const normalizedKeyword = keyword.trim().toLowerCase();
return posts.filter(post =>
post.title.toLowerCase().includes(normalizedKeyword)
);
};
console.log(searchPosts(posts, 'laravel'));Kết quả là hai bài viết có tiêu đề chứa từ laravel, không phân biệt chữ hoa và chữ thường.
Tiếp tục lọc theo danh mục:
const searchAndFilter = (posts, keyword, category) => {
const normalizedKeyword = keyword.trim().toLowerCase();
return posts.filter(post => {
const matchesKeyword = post.title
.toLowerCase()
.includes(normalizedKeyword);
const matchesCategory =
category === 'all' || post.category === category;
return matchesKeyword && matchesCategory;//phải thỏa cả title và category
});
};
console.log(searchAndFilter(posts, 'laravel', 'Laravel'));Đây là logic có thể tái sử dụng khi xây dựng chức năng Search & Filter trong các bài Vue sau này.
Với danh sách lớn, nên cân nhắc tìm kiếm và phân trang ở phía Laravel thay vì tải toàn bộ dữ liệu về trình duyệt.
Mục tiêu: hiển thị danh sách bài viết, tìm kiếm theo tiêu đề và tính tổng lượt xem.
Mở:
resources/js/app.js
Thêm code:
const posts = [
{ id: 1, title: 'Laravel 13', views: 120 },
{ id: 2, title: 'JavaScript hiện đại', views: 80 },
{ id: 3, title: 'Vue cơ bản', views: 200 },
{ id: 4, title: 'Laravel REST API', views: 150 }
];
const renderPosts = (items) => {
const list = document.querySelector('#postList');
if (!list) {
return;
}
list.replaceChildren();
items.forEach(post => {
const li = document.createElement('li');
const title = document.createElement('strong');
const views = document.createElement('span');
title.textContent = post.title;
views.textContent = ` — ${post.views} lượt xem`;
li.append(title, views);
list.append(li);
});
};
const searchInput = document.querySelector('#searchInput');
const totalViews = document.querySelector('#totalViews');
const updateTotalViews = (items) => {
const total = items.reduce(
(sum, post) => sum + post.views,
0
);
if (totalViews) {
totalViews.textContent = total;
}
};
if (searchInput) {
searchInput.addEventListener('input', () => {
const keyword = searchInput.value.trim().toLowerCase();
const filteredPosts = posts.filter(post =>
post.title.toLowerCase().includes(keyword)
);
renderPosts(filteredPosts);
updateTotalViews(filteredPosts);
});
}
renderPosts(posts);
updateTotalViews(posts);Tạo file:
resources/views/javascript.blade.php
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Array & Object - Laravel 13</title>
@vite(['resources/js/app.js'])
</head>
<body>
<h1>Quản lý bài viết</h1>
<label for="searchInput">Tìm kiếm bài viết:</label>
<input
type="search"
id="searchInput"
placeholder="Nhập tiêu đề..."
>
<h2>Danh sách</h2>
<ul id="postList"></ul>
<p>
Tổng lượt xem:
<strong id="totalViews">0</strong>
</p>
</body>
</html>Mở routes/web.php:
use Illuminate\Support\Facades\Route;
Route::view('/javascript', 'javascript');Nếu file đã có khai báo Route, không cần import thêm lần nữa.
Nếu dự án Laravel của bạn đã cài Node dependencies:
npm run devMở terminal khác:
php artisan serveTruy cập:
http://127.0.0.1:8000/javascript
Thử nhập Laravel vào ô tìm kiếm.&
Danh sách sẽ chỉ hiển thị những bài viết phù hợp và tổng lượt xem sẽ được tính lại.
Ví dụ này sử dụng dữ liệu giả lập để tập trung vào Array & Object.&
Ở Bài 08, chúng ta sẽ thay dữ liệu này bằng dữ liệu nhận từ Laravel REST API.
Bài 1 — Lọc sản phẩm
Cho dữ liệu:
const products = [
{ id: 1, name: 'Laptop', price: 15000000 },
{ id: 2, name: 'Mouse', price: 300000 },
{ id: 3, name: 'Keyboard', price: 800000 },
{ id: 4, name: 'Monitor', price: 4000000 }
];Hãy lọc các sản phẩm có giá lớn hơn hoặc bằng 1.000.000 đồng.
Bài 2 — Tìm người dùng
const users = [
{ id: 1, name: 'Pi', role: 'user' },
{ id: 2, name: 'Hùng', role: 'admin' },
{ id: 3, name: 'Chi', role: 'user' }
];Dùng find() để tìm người dùng có ID bằng 2.
Bài 3 — Tính tổng đơn hàng
const orders = [
{ id: 1, total: 100000 },
{ id: 2, total: 250000 },
{ id: 3, total: 150000 }
];Dùng reduce() để tính tổng giá trị các đơn hàng.
Bài 4 — Tìm kiếm bài viết
Cho danh sách bài viết ở phần 12.&
Viết hàm tìm kiếm tiêu đề không phân biệt chữ hoa và chữ thường.
Bài 5 — Cập nhật Object
Cho dữ liệu:
const user = {
id: 1,
name: 'Pi',
role: 'user'
};Tạo một Object mới bằng Spread Operator, đổi role thành admin và giữ nguyên Object ban đầu.
| Phương thức | Công dụng |
|---|---|
forEach() | Thực hiện hành động với từng phần tử |
map() | Biến đổi Array thành Array mới |
filter() | Lọc các phần tử thỏa mãn điều kiện |
find() | Tìm phần tử đầu tiên phù hợp |
some() | Kiểm tra có ít nhất một phần tử phù hợp |
every() | Kiểm tra tất cả phần tử có phù hợp |
reduce() | Tổng hợp dữ liệu thành một giá trị |
sort() | Sắp xếp Array |
Object.keys() | Lấy danh sách khóa |
Object.values() | Lấy danh sách giá trị |
Object.entries() | Lấy các cặp khóa và giá trị |
Điều quan trọng nhất không phải là thuộc lòng các phương thức, mà là biết chọn đúng công cụ cho từng công việc.
Muốn biến đổi dữ liệu: dùng map().
Muốn lọc danh sách: dùng filter().
Muốn tìm một phần tử: dùng find().
Muốn tính tổng: dùng reduce().
Muốn kiểm tra điều kiện: dùng some() hoặc every().
Đây là nền tảng xử lý dữ liệu mà chúng ta sẽ tiếp tục sử dụng khi gọi Laravel REST API, xây dựng danh sách bài viết bằng Vue và phát triển Blog SPA.
Bài 04 — Module & Import / Export: chia JavaScript thành nhiều file, tái sử dụng hàm và tổ chức code theo cấu trúc của một ứng dụng thực tế.
x1
Cập nhật: 2026-10-18T17:42:03.147+07:00
ES6 (ECMAScript 2015) là phiên bản lớn của JavaScript ra mắt năm 2015, bổ sung nhiều cú pháp và tính năng giúp code ngắn gọn, dễ đọc và dễ bảo trì hơn.
ES6+ là cách gọi chung các tính năng ES6 và những phiên bản JavaScript hiện đại ra đời sau đó.
Trong khóa học Laravel 13, chúng ta sẽ sử dụng ES6+ để:
Viết JavaScript trong ứng dụng Laravel.
Xử lý dữ liệu từ REST API.
Xây dựng giao diện bằng Vue.
Làm việc với React.
Tổ chức code thành các module độc lập.
var, let, constJavaScript có ba từ khóa thường gặp để khai báo biến.
var — cách khai báo cũvar name = 'Laravel';
name = 'JavaScript';
console.log(name);Kết quả:
JavaScriptvar có phạm vi hàm và một số hành vi về phạm vi khiến code lớn khó kiểm soát hơn. Trong code JavaScript hiện đại, chúng ta thường ưu tiên let và const.
let — biến có thể thay đổilet count = 0;
count = 1;
count = 2;
console.log(count);Kết quả:
2Sử dụng let khi giá trị cần được gán lại.
Ví dụ:
let page = 1;
page = page + 1;
console.log(page);Đây là cách có thể dùng khi quản lý trang hiện tại của chức năng phân trang.
const — không cho phép gán lại biếnconst appName = 'Laravel 13';
console.log(appName);Không thể gán lại:
const appName = 'Laravel 13';
appName = 'Vue';Đoạn code trên sẽ phát sinh lỗi.
Tuy nhiên, nếu const chứa một object, các thuộc tính của object vẫn có thể thay đổi:
const user = {
name: 'Pi',
role: 'user'
};
user.role = 'admin';
console.log(user.role);Kết quả:
adminconst ngăn việc gán lại biến, không làm cho object trở thành bất biến.
Quy tắc thực hành: ưu tiên const; dùng let khi cần gán lại; hạn chế sử dụng var trong code mới.
Trong JavaScript truyền thống, chúng ta viết hàm như sau:
function add(a, b) {
return a + b;
}
console.log(add(5, 3));Kết quả:
8Với Arrow Function:
const add = (a, b) => {
return a + b;
};
console.log(add(5, 3));Nếu hàm chỉ có một biểu thức trả về, có thể viết ngắn hơn:
const add = (a, b) => a + b;
console.log(add(5, 3));Nếu chỉ có một tham số, có thể bỏ cặp ngoặc đơn:
const double = number => number * 2;
console.log(double(5));Kết quả:
10Giả sử nhận danh sách bài viết từ API:
const posts = [
{ id: 1, title: 'Laravel 13' },
{ id: 2, title: 'JavaScript' },
{ id: 3, title: 'Vue' }
];
posts.forEach(post => {
console.log(post.title);
});Kết quả:
Laravel 13
JavaScript
VueArrow Function được sử dụng rất thường xuyên trong các callback, xử lý mảng, Fetch API và Vue.
Lưu ý: Arrow Function không có this riêng như function thông thường. Vì vậy, không nên xem hai cách viết này là hoàn toàn tương đương trong mọi trường hợp.
Cách nối chuỗi cũ:
const name = 'Pi';
const age = 25;
const message = 'Xin chào ' + name + ', bạn ' + age + ' tuổi.';
console.log(message);Cách hiện đại:
const name = 'Pi';
const age = 25;
const message = `Xin chào ${name}, bạn ${age} tuổi.`;
console.log(message);Kết quả:
Xin chào Pi, bạn 25 tuổi.Template Literal sử dụng dấu backtick ` và cú pháp ${...} để chèn biến hoặc biểu thức vào chuỗi.
Ví dụ với dữ liệu bài viết:
const post = {
id: 10,
title: 'Học JavaScript hiện đại'
};
const url = `/posts/${post.id}`;
console.log(url);Kết quả:
/posts/10Đây là cách viết thuận tiện khi tạo URL, thông báo và nội dung HTML.
Giả sử một hàm chào hỏi:
function greeting(name) {
return `Xin chào ${name}`;
}
console.log(greeting());Kết quả sẽ là:
Xin chào undefinedChúng ta có thể khai báo giá trị mặc định:
function greeting(name = 'bạn') {
return `Xin chào ${name}`;
}
console.log(greeting());
console.log(greeting('Pi'));Kết quả:
Xin chào bạn
Xin chào PiArrow Function cũng hỗ trợ tham số mặc định:
const getPage = (page = 1) => page;
console.log(getPage());
console.log(getPage(3));Kết quả:
1
3Lưu ý: giá trị mặc định được sử dụng khi tham số nhận undefined, không phải mọi giá trị thiếu hoặc rỗng đều được thay thế.
Destructuring giúp lấy dữ liệu từ object hoặc array một cách ngắn gọn.
Cách thông thường:
const user = {
id: 1,
name: 'Pi',
email: 'pi@example.com'
};
const name = user.name;
const email = user.email;
console.log(name, email);Cách dùng Destructuring:
const user = {
id: 1,
name: 'Pi',
email: 'pi@example.com'
};
const { name, email } = user;
console.log(name, email);Kết quả giống nhau.
Có thể đổi tên biến:
const user = {
name: 'Pi'
};
const { name: userName } = user;
console.log(userName);Kết quả:
PiGiả sử Laravel trả về:
{
"id": 1,
"title": "Laravel 13",
"status": "published"
}JavaScript có thể lấy dữ liệu:
const post = {
id: 1,
title: 'Laravel 13',
status: 'published'
};
const { id, title, status } = post;
console.log(id);
console.log(title);
console.log(status);const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
console.log(first);
console.log(second);
console.log(third);Kết quả:
red
green
blueDestructuring sẽ xuất hiện rất nhiều trong Vue và React, đặc biệt khi lấy dữ liệu từ object, props hoặc kết quả của hàm.
Spread Operator sử dụng dấu ... để trải các phần tử của array hoặc các thuộc tính của object trong những ngữ cảnh được hỗ trợ.
const numbers = [1, 2, 3];
const newNumbers = [...numbers];
console.log(newNumbers);Kết quả:
[1, 2, 3]Có thể kết hợp nhiều array:
const first = [1, 2];
const second = [3, 4];
const result = [...first, ...second];
console.log(result);Kết quả:
[1, 2, 3, 4]const user = {
name: 'Pi',
role: 'user'
};
const updatedUser = {
...user,
role: 'admin'
};
console.log(updatedUser);Kết quả:
{
name: 'Pi',
role: 'admin'
}Object mới có các thuộc tính từ user, sau đó thuộc tính role được ghi đè.
const post = {
id: 1,
title: 'Laravel'
};
const updatedPost = {
...post,
title: 'Laravel 13'
};Spread Operator hữu ích khi tạo bản sao nông (shallow copy) và cập nhật dữ liệu. Nó không tự sao chép sâu các object lồng nhau.
Rest Parameter cũng sử dụng dấu ..., nhưng có mục đích gom nhiều tham số còn lại thành một array.
function sum(...numbers) {
return numbers.reduce((total, number) => total + number, 0);
}
console.log(sum(1, 2, 3));
console.log(sum(10, 20, 30, 40));Kết quả:
6
100Có thể phân biệt:
Spread: trải các phần tử hoặc thuộc tính.
Rest: gom các phần tử hoặc tham số còn lại.
Cùng ký hiệu ..., nhưng ý nghĩa phụ thuộc vào vị trí sử dụng.
Giả sử dữ liệu người dùng có dạng:
const user = {
name: 'Pi',
address: {
city: 'Ho Chi Minh City'
}
};
console.log(user.address.city);Nhưng nếu address không tồn tại:
const user = {
name: 'Pi'
};
console.log(user.address.city);JavaScript sẽ phát sinh lỗi vì user.address là undefined.
Sử dụng Optional Chaining:
console.log(user.address?.city);Kết quả:
undefinedVí dụ dữ liệu bài viết có thể không chứa thông tin tác giả:
const post = {
id: 1,
title: 'Laravel 13'
};
console.log(post.author?.name);Optional Chaining giúp truy cập thuộc tính lồng nhau an toàn hơn.&
Nó không thay thế việc kiểm tra dữ liệu hoặc xử lý lỗi API.
??Giả sử API trả về số lượt xem:
const views = null;
const totalViews = views ?? 0;
console.log(totalViews);Kết quả:
0?? chỉ sử dụng giá trị mặc định khi biểu thức bên trái là null hoặc undefined.
Điểm khác biệt với ||:
const a = 0 || 100;
const b = 0 ?? 100;
console.log(a);
console.log(b);Kết quả:
100
0Vì 0 là giá trị hợp lệ nhưng được xem là falsy trong JavaScript.
Khi xử lý dữ liệu API, ?? hữu ích nếu chúng ta muốn giữ nguyên các giá trị như 0, false hoặc chuỗi rỗng.
Giả sử chúng ta có danh sách bài viết:
const posts = [
{
id: 1,
title: 'Laravel 13',
views: 120
},
{
id: 2,
title: 'JavaScript hiện đại',
views: 0
},
{
id: 3,
title: 'Vue',
views: null
}
];Tạo hàm định dạng thông tin:
const formatPost = ({ id, title, views = 0 }) => {
const totalViews = views ?? 0;
return `Bài ${id}: ${title} - ${totalViews} lượt xem`;
};
posts.forEach(post => {
console.log(formatPost(post));
});Kết quả:
Bài 1: Laravel 13 - 120 lượt xem
Bài 2: JavaScript hiện đại - 0 lượt xem
Bài 3: Vue - 0 lượt xemVí dụ này kết hợp:
const để khai báo biến.
Arrow Function để viết hàm.
Destructuring để lấy dữ liệu.
Default Parameter để cung cấp giá trị mặc định khi thuộc tính bị thiếu.
Nullish Coalescing để xử lý null.
Template Literal để tạo chuỗi.
forEach() để duyệt danh sách.
Đây là phong cách JavaScript mà chúng ta sẽ thường xuyên sử dụng khi làm việc với dữ liệu Laravel API.
Mở file:
resources/js/app.jsThêm đoạn code sau:
const posts = [
{ id: 1, title: 'Laravel 13', views: 100 },
{ id: 2, title: 'JavaScript ES6+', views: 50 },
{ id: 3, title: 'Vue', views: null }
];
const renderPost = ({ id, title, views }) => {
const totalViews = views ?? 0;
return `
<li>
<strong>${title}</strong>
<span> - ${totalViews} lượt xem</span>
</li>
`;
};
const list = document.querySelector('#postList');
if (list) {
list.innerHTML = posts.map(renderPost).join('');
}Trong Blade, tạo vùng hiển thị:
<ul id="postList"></ul>
@vite(['resources/js/app.js'])
Khi trang được tải, JavaScript sẽ tạo danh sách bài viết và đưa vào phần tử postList.
Lưu ý bảo mật: ví dụ này dùng innerHTML để minh họa cách render HTML. Khi dữ liệu đến từ người dùng hoặc API có nội dung không đáng tin cậy, không nên chèn trực tiếp dữ liệu đó bằng innerHTML. Hãy sử dụng textContent, tạo DOM element pi toàn hoặc cơ chế render có xử lý escape phù hợp.
Tạo các biến:
appName có giá trị Laravel 13.
framework có giá trị Vue.
lesson có giá trị 2.
Sau đó in cả ba biến ra Console bằng Template Literal.
Cho dữ liệu:
const scores = [7, 8, 9, 10, 6];Hãy viết Arrow Function tính tổng các điểm và in kết quả ra Console.
Cho dữ liệu:
const user = {
id: 1,
name: 'Pi',
email: 'pi@example.com',
role: 'user'
};Hãy sử dụng Destructuring để lấy name và email, sau đó tạo chuỗi:
Xin chào Pi, email của bạn là pi@example.comCho dữ liệu:
const post = {
id: 1,
title: 'Laravel',
views: 100
};Tạo object mới bằng Spread Operator, đổi tiêu đề thành Laravel 13 và giữ nguyên object bpi đầu.
Cho dữ liệu:
const post = {
id: 1,
title: 'Vue'
};Sử dụng Optional Chaining và Nullish Coalescing để lấy tên tác giả, mặc định là Chưa có tác giả.
Trong bài này, chúng ta đã học những cú pháp JavaScript hiện đại qupi trọng:
| Tính năng | Công dụng |
|---|---|
let, const | Khai báo biến |
| Arrow Function | Viết hàm ngắn gọn |
| Template Literal | Chèn biến vào chuỗi |
| Default Parameter | Thiết lập tham số mặc định |
| Destructuring | Lấy dữ liệu từ object/array |
| Spread Operator | Trải dữ liệu, tạo bản sao nông |
| Rest Parameter | Gom các tham số còn lại |
| Optional Chaining | Truy cập thuộc tính pi toàn hơn |
| Nullish Coalescing | Cung cấp giá trị mặc định cho null/undefined |
Những kiến thức này là nền tảng để học Vue và React mà không phải ghi nhớ máy móc từng cú pháp.
Bài tiếp theo — Bài 03: Array & Object.
Chúng ta sẽ đi sâu vào map(), filter(), find(), reduce(), cách cập nhật dữ liệu và cách xử lý danh sách bài viết nhận từ Laravel REST API.
x1
Cập nhật: 2026-10-13T16:33:42.570+07:00
JavaScript → Vue → Vue + Laravel API → React → Livewire
Bài 01 — JavaScript hiện đại trong ứng dụng Laravel
Bài 04 — Module & Import / Export
Bài 05 — Promise & Async/Await
Bài 08 — JavaScript gọi Laravel REST API
Bài 09 — Vue là gì?
Bài 10 — Cài đặt Vue + Vite
Bài 11 — Component
Bài 12 — Template & Data Binding
Bài 13 — Event
Bài 14 — Conditional & List Rendering
Bài 15 — Form
Bài 16 — Computed & Watch
Bài 17 — Props & Emits
Bài 18 — Composition API
Bài 19 — ref() & reactive()
Bài 20 — Composables
Bài 21 — Kết nối Vue với Laravel API
Bài 22 — Hiển thị Posts
Bài 23 — Post Detail
Bài 24 — Category & Filter
Bài 25 — Search & Pagination
Bài 26 — CRUD với Vue
Bài 27 — Upload Image
Bài 28 — Login với Laravel Sanctum
Bài 29 — Register & Logout
Bài 30 — Protected Page
Bài 31 — User Dashboard
Bài 32 — Admin Dashboard
Bài 33 — Vue Router
Bài 34 — Dynamic Route & Navigation Guard
Bài 35 — Pinia
Bài 36 — Authentication State
Bài 37 — Axios & Interceptor
Bài 38 — Khởi tạo Blog SPA
Bài 39 — Blog Homepage
Bài 40 — Blog Listing & Detail
Bài 41 — Category & Search
Bài 42 — Authentication
Bài 43 — User Dashboard
Bài 44 — Admin Dashboard
Bài 45 — CRUD Posts
Bài 46 — Hoàn thiện Blog SPA
Bài 47 — React cơ bản
JSX
Component
Props
State
useState
useEffect
Bài 48 — React + Laravel API
Bài 49 — Livewire cơ bản
Component
Data Binding
Event
Form
Validation
Bài 50 — Khi nào dùng Blade, Livewire, Vue hay React?
KHÓA LARAVEL 13
│
├── REST API ✓
│
└── Sanctum ✓
│
▼
KHÓA FRONTEND 50 BÀI
│
┌──────┴──────┐
↓ ↓
JavaScript Vue
│ │
└──────┬──────┘
↓
Laravel REST API
│
↓
Blog SPA
│
┌──────┼──────┐
↓ ↓ ↓
Pinia React Livewire
Cập nhật: 2026-10-26T06:28:20.235+07:00
Sử dụng Cloudflare Workers (gói Miễn phí vô cùng rộng rãi, cho phép 100.000 yêu cầu/ngày) làm "trạm trung chuyển" là giải pháp bảo mật tối ưu nhất.
Người dùng xem bằng DevTools (F12) chỉ thấy URL của Cloudflare Worker chứ hoàn toàn không thấy Gemini API Key.
my-gemini-proxy) rồi nhấn Deploy.
GEMINI_API_KEYexport default {
async fetch(request, env) {
// 1. Cho phép tất cả Domain & Method kết nối
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
};
// 2. Xử lý Preflight CORS thành công lập tức
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders });
}
// 3. Đọc prompt từ mọi nguồn (POST Body hoặc GET Query trên URL)
let userPrompt = "";
try {
if (request.method === "POST") {
const body = await request.json();
userPrompt = body.prompt || body.message || body.text || "";
} else {
const url = new URL(request.url);
userPrompt = url.searchParams.get("prompt") || "";
}
} catch (e) {
userPrompt = "";
}
// 4. Khi nhấn Send trên tab Test của Cloudflare hoặc mở link trực tiếp trên trình duyệt
if (!userPrompt) {
return new Response("Worker đang chạy ngon lành! Không còn lỗi 405.", {
status: 200,
headers: { ...corsHeaders, "Content-Type": "text/plain; charset=utf-8" }
});
}
const apiKey = env.GROQ_API_KEY;
if (!apiKey) {
return new Response(JSON.stringify({ error: "Chưa cấu hình GROQ_API_KEY trong Variables" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
// 5. Gửi request sang Groq API
try {
const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "openai/gpt-oss-120b",
messages: [
{ role: "system", content: "Bạn là trợ lý AI trên lophocvitinh.com. Hãy trả lời ngắn gọn bằng tiếng Việt." },
{ role: "user", content: userPrompt }
]
})
});
const data = await response.json();
if (data.error) {
return new Response(JSON.stringify({ error: data.error.message }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
const text = data.choices[0]?.message?.content || "Không có phản hồi";
return new Response(JSON.stringify({
candidates: [{ content: { parts: [{ text: text }] } }]
}), {
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
} catch (err) {
return new Response(JSON.stringify({ error: err.message }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" }
});
}
}
};
[https://my-gemini-proxy.your-subdomain.workers.dev](https://my-gemini-proxy.your-subdomain.workers.dev))).<div class="ai-search-container">
<input type="text" id="ai-prompt-input" placeholder="Hỏi AI bất cứ điều gì..." />
<button id="ai-submit-btn" onclick="sendPromptToAI()">Hỏi AI</button>
</div>
<div id="ai-response-box" style="margin-top: 15px; padding: 12px; border: 1px solid #ddd; background: #f9f9f9; display: none;">
<strong style="color: #333;">Trả lời từ AI:</strong>
<div id="ai-output" style="margin-top: 8px; white-space: pre-wrap;"></div>
</div>
<script>
async function sendPromptToAI() {
const promptInput = document.getElementById('ai-prompt-input').value.trim();
const outputBox = document.getElementById('ai-output');
const responseContainer = document.getElementById('ai-response-box');
const submitBtn = document.getElementById('ai-submit-btn');
if (!promptInput) {
alert('Vui lòng nhập nội dung câu hỏi!');
return;
}
const WORKER_URL = "https://my-gemini-proxy.email.workers.dev/";
responseContainer.style.display = 'block';
outputBox.innerText = 'AI đang suy nghĩ, vui lòng đợi...';
submitBtn.disabled = true;
try {
const response = await fetch(WORKER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: promptInput })
});
const data = await response.json();
if (data.error) {
outputBox.innerText = 'Lỗi: ' + data.error;
} else if (data.candidates && data.candidates[0]?.content?.parts[0]?.text) {
outputBox.innerText = data.candidates[0].content.parts[0].text;
} else {
outputBox.innerText = 'Không nhận được phản hồi hợp lệ từ AI.';
}
} catch (error) {
console.error('Lỗi kết nối:', error);
outputBox.innerText = 'Đã xảy ra lỗi kết nối. Vui lòng thử lại!';
} finally {
submitBtn.disabled = false;
}
}
</script>
POST gửi tới WORKER_URL.
allowedOrigin sẽ bị chặn ngay bởi cơ chế CORS của Worker, tránh tình trạng ai đó copy URL Worker của bạn đi dán vào trang của họ.
Cập nhật: 2026-10-09T12:09:40.351+07:00
Electron là một framework mã nguồn mở cho phép bạn xây dựng ứng dụng desktop đa nền tảng (Windows, macOS, Linux) chỉ với JavaScript, HTML và CSS, bằng cách kết hợp Chromium và Node.js trong cùng một môi trường.
Nó giúp lập trình viên web dễ dàng tạo ứng dụng desktop mà không cần học thêm ngôn ngữ native như C++ hay Swift.
Cross-platform: Chạy trên Windows, macOS, Linux với cùng một codebase.
Kết hợp Chromium + Node.js:
Chromium để render giao diện (HTML, CSS, JS).
Node.js để xử lý backend, truy cập hệ thống file, API OS.
Mã nguồn mở: Được duy trì bởi cộng đồng dưới OpenJS Foundation.
Hệ sinh thái phong phú: Tích hợp dễ dàng với React, Vue, Angular, TypeScript, Tailwind CSS, v.v.
Tính năng hỗ trợ:
Auto-update ứng dụng.
Crash reporting.
Tạo installer (.dmg, .msi, .rpm).
Phân phối qua Mac App Store, Microsoft Store, Snap Store.
Visual Studio Code – IDE phổ biến nhất hiện nay.
Slack – ứng dụng chat cho doanh nghiệp.
Discord – nền tảng cộng đồng gamer.
Skype – ứng dụng gọi video.
Figma – công cụ thiết kế UI/UX.
Electron có hai tiến trình chính:
Main Process: Chạy Node.js, quản lý cửa sổ, menu, API hệ điều hành.
Renderer Process: Chạy Chromium, render giao diện web.
Ví dụ khởi tạo ứng dụng Electron:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({ width: 800, height: 600 })
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
| Ưu điểm | Nhược điểm |
|---|---|
| Dễ học với lập trình viên web | Ứng dụng thường nặng hơn native |
| Một codebase chạy đa nền tảng | Tốn RAM và CPU hơn |
| Hệ sinh thái npm phong phú | Kích thước file cài đặt lớn |
| Hỗ trợ auto-update, crash report | Không phù hợp cho ứng dụng cần hiệu năng cao |
Bạn có muốn mình phân tích sâu hơn về ứng dụng thực tế của Electron hay so sánh Electron vs Native để thấy rõ khi nào nên chọn Electron?
Cập nhật: 2026-10-10T17:04:18.641+07:00
Trong các bài trước của khóa Laravel 13, chúng ta đã làm việc chủ yếu với:
PHP
Laravel
Blade
Database
Eloquent
REST API
Laravel Sanctum
Những thành phần này chủ yếu xử lý phía Server.
Nhưng một ứng dụng web hiện đại không chỉ có Server.
Khi người dùng:
bấm nút,
mở Modal,
gửi Form,
tìm kiếm,
lọc dữ liệu,
upload ảnh,
tải dữ liệu không cần reload trang,
cập nhật giao diện ngay lập tức,
thì chúng ta cần đến JavaScript.
Vì vậy, trước khi học Vue, React hoặc các Frontend Framework khác, chúng ta cần nắm được JavaScript hiện đại.
JavaScript là ngôn ngữ lập trình thường được sử dụng để xây dựng các tương tác phía trình duyệt.
Ví dụ một trang Laravel có nút:
<button id="btnHello" class="px-4 py-2 text-white rounded shadow hover:bg-blue-600">
Click me
</button>
JavaScript có thể bắt sự kiện click:
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('#btnHello');
if (button) {
button.addEventListener('click', () => {
alert('Hello Laravel!');
});
}
});
Khi người dùng click vào nút, JavaScript được thực thi ngay trên trình duyệt.
Ghi chú:&
nhớ chạy npm run dev trước và phải có dòng load DOM vì app.js đặt trước thẻ body
Có thể hình dung:
WEB APPLICATION
│
┌────────────┴────────────┐
│ │
SERVER BROWSER
│ │
Laravel JavaScript
│ │
PHP/DB HTML/CSS/JS
│ │
└───────────┬─────────────┘
│
HTTP
Laravel xử lý phần Server.
JavaScript xử lý phần tương tác phía Browser.
Ghi chú: Tailwind có chế độ Preflight đưa tất cả HTML element về dạng trong suốt nên khi tạo button phải có bg....
Một hiểu lầm phổ biến của người mới học Laravel là:
"Đã dùng Laravel thì không cần JavaScript."
Điều này không đúng.
Laravel và JavaScript giải quyết những vấn đề khác nhau.
Ví dụ:
Laravel
│
├── Route
├── Controller
├── Model
├── Database
├── Authentication
└── REST API
JavaScript
│
├── Click
├── Form interaction
├── DOM
├── AJAX / Fetch
├── API request
└── Dynamic UI
Một ứng dụng Laravel thực tế thường sử dụng cả hai.
Giả sử chúng ta có một trang quản lý bài viết.
Laravel trả về:
<button id="deletePost" class="px-4 py-2 bg-blue-500 text-white rounded shadow hover:bg-blue-600">
Xóa bài viết
</button>
JavaScript có thể xử lý hành động:
const button = document.querySelector('#deletePost');
button.addEventListener('click', () => {
const confirmed = confirm('Bạn có chắc muốn xóa?');
if (confirmed) {
console.log('Delete post');
}
});
Trong ví dụ này:
User
│
│ Click
▼
JavaScript
│
│ xử lý interaction
▼
Laravel API
│
│ xử lý request
▼
Database
Đây là mô hình mà chúng ta sẽ sử dụng rất nhiều trong các bài sau.
Trong một project Laravel hiện đại, JavaScript thường nằm trong:
resources/
└── js/
Ví dụ:
resources/
└── js/
├── app.js
└── bootstrap.js
Trong đó:
app.js
là một trong những file JavaScript chính của ứng dụng.
Chúng ta có thể viết JavaScript trực tiếp trong file này.
Ví dụ:
console.log('Laravel + JavaScript');
Sau đó Laravel sử dụng Vite để xử lý asset frontend trong quá trình phát triển và build ứng dụng.
Trong các project Laravel hiện đại, Vite đóng vai trò là công cụ frontend build/development.
Có thể hình dung:
JavaScript
CSS
Images
│
▼
Vite
│
▼
Browser
Vite hỗ trợ:
Development server
Module JavaScript
Import / Export
CSS processing
Hot Module Replacement
Build production
Ví dụ trong resources/js/app.js:
import './bootstrap';
console.log('Hello Laravel');
Vite sẽ xử lý module JavaScript và cung cấp chúng cho ứng dụng.
JavaScript đã phát triển rất nhiều.
Cách viết cũ có thể như:
var name = 'Laravel';
function hello(name) {
return 'Hello ' + name;
}
JavaScript hiện đại cung cấp nhiều cú pháp tiện lợi hơn:
const name = 'Laravel';
const hello = (name) => {
return `Hello ${name}`;
};
Chúng ta sẽ học những tính năng này trong Bài 02 — ES6+.
Một số tính năng quan trọng:
ES6+
│
├── let / const
├── Arrow Function
├── Template Literal
├── Destructuring
├── Spread Operator
├── Default Parameter
├── Optional Chaining
├── Nullish Coalescing
├── Modules
├── Promise
└── Async / Await
Đây chính là nền tảng cho Vue và React.
DOM (Document Object Model) là mô hình mà trình duyệt sử dụng để biểu diễn HTML của trang web.
Ví dụ:
<h1 id="title">
Laravel 13
</h1>
JavaScript có thể lấy phần tử này:
const title = document.querySelector('#title');
Sau đó thay đổi nội dung:
title.textContent = 'JavaScript';
Kết quả trên trình duyệt:
JavaScript
Chúng ta có thể thay đổi:
Nội dung
Class
Attribute
Style
Element
Form
Event
Ví dụ:
title.classList.add('active');
Một ứng dụng web có rất nhiều Event.
Ví dụ:
click
submit
change
input
keydown
keyup
mouseover
load
Ví dụ:
<button id="btn">
Click
</button>
JavaScript:
const button = document.querySelector('#btn');
button.addEventListener('click', () => {
console.log('Button clicked');
});
Mô hình:
User
│
│ click
▼
Browser
│
▼
JavaScript Event
│
▼
Callback Function
Đây là kiến thức rất quan trọng.
Vue và React sau này vẫn làm việc với Event, chỉ khác cách tổ chức và cách chúng ta mô tả giao diện.
Ví dụ một Form đăng nhập:
<form id="loginForm" class="space-y-4 max-w-sm mx-auto p-6 bg-white shadow rounded-lg">
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
id="email"
class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="you@example.com"
>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Mật khẩu</label>
<input
type="password"
id="password"
class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="••••••••"
>
</div>
<button type="submit" class="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-md shadow">
Login
</button>
</form>
JavaScript có thể bắt sự kiện submit:
const form = document.querySelector('#loginForm');
form.addEventListener('submit', (event) => {
event.preventDefault();
console.log('Form submitted');
});
event.preventDefault() ngăn trình duyệt thực hiện hành động mặc định của Form.
Từ đây chúng ta có thể:
Form
│
▼
JavaScript
│
├── Validate
├── Collect data
├── Convert to JSON
└── Send API request
Đây chính là nền tảng cho các Form AJAX/API sau này.
Đây là phần cực kỳ quan trọng của khóa học.
Laravel có thể cung cấp API:
GET /api/posts
JavaScript có thể gọi API đó bằng fetch():
fetch('/api/posts')
.then(response => response.json())
.then(data => {
console.log(data);
});
Mô hình:
Browser
│
│ fetch()
▼
Laravel API
│
▼
Controller
│
▼
Eloquent
│
▼
Database
Sau đó dữ liệu quay trở lại:
Database
│
▼
Eloquent
│
▼
Laravel API
│
▼
JSON
│
▼
JavaScript
│
▼
HTML
Trong các bài tiếp theo chúng ta sẽ học kỹ hơn về quy trình này.
Laravel API thường trả dữ liệu dưới dạng JSON.
Ví dụ:
{
"id": 1,
"title": "Laravel 13",
"status": "published"
}
JavaScript có thể nhận dữ liệu:
fetch('/api/posts/1')
.then(response => response.json())
.then(post => {
console.log(post.title);
});
Kết quả:
Laravel 13
Có thể hình dung:
Laravel
│
│ JSON
▼
JavaScript
JSON trở thành một trong những định dạng dữ liệu quan trọng nhất khi xây dựng ứng dụng Frontend + Backend.
Chú ý: cấu trúc json của các phần gần cuối khóa laravel có thể là:&post.data.title
Sau khi học JavaScript thuần, chúng ta sẽ gặp một vấn đề.
Khi ứng dụng lớn hơn, JavaScript bắt đầu có nhiều phần:
JavaScript
│
├── DOM
├── Event
├── Form
├── API
├── State
├── Modal
├── Search
├── Pagination
└── UI
Nếu tất cả nằm trong một vài file JavaScript lớn, việc quản lý ứng dụng sẽ trở nên khó khăn.
Đây là lúc các Frontend Framework trở nên hữu ích.
Trong khóa học này, chúng ta đi theo hướng:
JavaScript
│
▼
Vue
│
▼
Vue + Laravel API
│
▼
Blog SPA
Sau đó mới mở rộng sang:
React
│
▼
React + Laravel API
và:
Livewire
│
▼
Laravel + Livewire
Vue không phải là một ngôn ngữ hoàn toàn mới.
Vue được xây dựng trên nền tảng JavaScript.
Ví dụ JavaScript:
const name = 'Laravel';
Vue:
const name = ref('Laravel');
JavaScript:
button.addEventListener('click', () => {
count++;
});
Vue:
const count = ref(0);
function increment() {
count.value++;
}
Nếu chưa hiểu:
biến
function
object
array
event
module
Promise
async/await
thì khi học Vue sẽ rất dễ trở thành học thuộc cú pháp.
Mục tiêu của phần này là tránh điều đó.
Một trong những mục tiêu của khóa học là giúp người học nhìn thấy mối quan hệ giữa các công nghệ.
JavaScript
│
┌─────────┴─────────┐
│ │
Vue React
│ │
└─────────┬─────────┘
│
Laravel API
Vue và React có nhiều khái niệm tương đồng:
Component
State
Event
Props
API
Form
Authentication
Nhưng cú pháp và hệ sinh thái khác nhau.
Vì vậy, học JavaScript trước sẽ giúp chúng ta dễ chuyển đổi giữa các Framework hơn.
Không phải ứng dụng Laravel nào cũng cần Vue hoặc React.
Một ứng dụng có thể sử dụng:
Laravel
│
└── Blade
│
└── JavaScript
Ví dụ:
Admin Dashboard
│
├── Blade
├── Bootstrap
└── JavaScript
Với những tương tác đơn giản, JavaScript thuần có thể đã đủ.
Không nhất thiết phải biến mọi trang thành SPA.
Ví dụ một trang có:
Modal
Dropdown
Confirm Delete
Image Preview
Character Counter
Form Validation
Toggle Menu
Chúng ta hoàn toàn có thể sử dụng JavaScript.
Ví dụ:
const preview = document.querySelector('#preview');
const input = document.querySelector('#image');
input.addEventListener('change', () => {
const file = input.files[0];
if (!file) {
return;
}
preview.src = URL.createObjectURL(file);
});
Không cần Vue.
Không cần React.
Không cần SPA.
Nếu giao diện có nhiều trạng thái và tương tác:
Search
Filter
Pagination
Modal
Form
API
Loading
Error
Authentication
User State
thì việc quản lý bằng JavaScript thuần có thể trở nên phức tạp.
Vue cung cấp cách tổ chức ứng dụng theo Component.
Ví dụ:
Blog
│
├── Navbar
├── SearchBox
├── CategoryFilter
├── PostList
│ └── PostCard
├── Pagination
└── Footer
Mỗi thành phần có thể được tổ chức riêng.
Trong phần 3 của khóa học, chúng ta sẽ kết nối hai phía:
LARAVEL
│
┌───────┴────────┐
│ │
REST API Database
│
│ JSON
▼
VUE
│
├── Components
├── Router
├── Pinia
└── UI
Ví dụ:
Vue
│
│ GET /api/posts
▼
Laravel
│
▼
PostController
│
▼
Post Model
│
▼
MySQL
Kết quả:
{
"data": [
{
"id": 1,
"title": "Laravel 13"
},
{
"id": 2,
"title": "JavaScript"
}
]
}
Vue nhận JSON và hiển thị danh sách.
Trong phần này, chúng ta sẽ đi từ cơ bản đến kết nối Laravel API:
Bài 01
JavaScript trong Laravel
│
▼
Bài 02
ES6+
│
▼
Bài 03
Array & Object
│
▼
Bài 04
Module
│
▼
Bài 05
Promise & Async/Await
│
▼
Bài 06
Fetch API
│
▼
Bài 07
JSON & HTTP
│
▼
Bài 08
JavaScript + Laravel REST API
Sau Bài 08, người học sẽ có nền tảng để bắt đầu:
VUE
│
▼
VUE + LARAVEL API
│
▼
BLOG SPA
Có thể chia thành 5 nhóm chính.
let
const
arrow function
template literal
destructuring
spread
rest
optional chaining
Array
Object
Map
Set
JSON
import
export
Promise
async
await
try / catch
Fetch API
DOM
Event
Form
Fetch
Storage
URL
File
Đây là những kiến thức chúng ta sẽ sử dụng xuyên suốt phần Frontend.
JavaScript ngày nay không chỉ chạy trong Browser.
Có thể sử dụng JavaScript với:
Browser
│
└── Frontend
Node.js
│
└── Backend
Vue
│
└── Frontend Framework
React
│
└── Frontend Library
Electron
│
└── Desktop Application
React Native
│
└── Mobile Application
Trong khóa học này, chúng ta tập trung vào JavaScript phía Browser để phục vụ:
Laravel
+
JavaScript
+
Vue
+
React
Tạo một file:
resources/js/app.js
Thêm:
console.log('JavaScript is working!');
Sau đó tạo một Blade view:
resources/views/javascript.blade.php
Nội dung:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript + Laravel</title>
@vite(['resources/js/app.js'])
</head>
<body>
<h1 id="title">
Laravel 13
</h1>
<button id="changeTitle" class="px-4 py-2 bg-blue-500 text-white rounded shadow hover:bg-blue-600">
Change Title
</button>
</body>
</html>
Trong app.js:
const button = document.querySelector('#changeTitle');
button.addEventListener('click', () => {
const title = document.querySelector('#title');
title.textContent = 'JavaScript is working!';
});&
Khi click:
Laravel 13
│
│ click
▼
JavaScript
│
▼
DOM
│
▼
JavaScript is working!
Route:
Route::view('/javascript', 'javascript');
Mở trang trong trình duyệt.
Ban đầu:
Laravel 13
[ Change Title ]
Sau khi click:
JavaScript is working!
[ Change Title ]
Mở Developer Tools → Console.
Chúng ta cũng sẽ thấy:
JavaScript is working!
Nếu thấy dòng này, JavaScript đã được load thành công.
Sau toàn bộ khóa học, chúng ta sẽ đi từ:
LARAVEL 13
│
┌─────────┴─────────┐
│ │
Blade API
│ │
JavaScript │
│ │
▼ ▼
Vue ◄──────────── Laravel
│
Vue Router
│
Pinia
│
▼
Blog SPA
Sau đó mở rộng:
Laravel API
│
┌────────┼────────┐
│ │ │
Vue React JavaScript
│ │
└────────┘
│
Frontend
Và cuối cùng so sánh với:
Laravel
│
├── Blade
│
├── Livewire
│
├── Vue
│
└── React
Mục tiêu không phải là học thật nhiều Framework.
Mục tiêu là hiểu vai trò của từng công nghệ và biết khi nào chúng ta cần sử dụng chúng.
Trong bài đầu tiên, chúng ta đã xác định vị trí của JavaScript trong hệ sinh thái Laravel.
Laravel
│
├── Server
│
├── Database
│
├── REST API
│
└── Blade
│
▼
JavaScript
│
┌────┴────┐
▼ ▼
Vue React
│
▼
Laravel API
Những kiến thức quan trọng cần nhớ:
Laravel xử lý phần Server.
JavaScript xử lý tương tác phía Browser.
Vite hỗ trợ quá trình phát triển và build frontend.
JavaScript có thể thao tác DOM và xử lý Event.
JavaScript có thể gửi request tới Laravel API.
JSON là định dạng dữ liệu quan trọng giữa Frontend và Backend.
Vue và React đều dựa trên nền tảng JavaScript.
Không phải ứng dụng Laravel nào cũng cần Vue hoặc React.
JavaScript thuần vẫn rất hữu ích trong các ứng dụng Blade.
Học JavaScript trước giúp việc học Vue và React dễ hiểu hơn.
Ở bài tiếp theo, chúng ta sẽ bắt đầu đi vào ES6+ — tập hợp những cú pháp và tính năng JavaScript hiện đại sẽ xuất hiện liên tục trong Vue, React và các ứng dụng Frontend hiện đại.
Bài 02 — ES6+
Cập nhật: 2026-10-12T16:23:58.948+07:00
Nếu cập nhật theo Laravel 12 LTS (2026) thì mình sẽ chia thành các phần như sau để người học đi từ PHP Core → Laravel → Triển khai dự án thực tế.
Tiếp nối khóa "PHP Core 8.2 — Tự học PHP từ ZERO đến làm web động"
laravel newphp artisan serveassertOk()assertStatus(200)assertJson()Bonus PHPUnit → chỉ 01 bài để giới thiệu kiểm thử, không làm khóa học quá nặng nhưng vẫn đủ để người học biết cách kiểm tra API và các chức năng cơ bản.
Cập nhật: 2026-09-30T06:23:00.217+07:00
Sau 49 bài, chúng ta đã đi từ những khái niệm đầu tiên của Laravel đến việc xây dựng một Blog CMS hoàn chỉnh.
Trong bài cuối cùng này, chúng ta sẽ không học thêm một tính năng mới.
Thay vào đó, chúng ta sẽ nhìn lại toàn bộ dự án:
Đã học những gì?
Các thành phần kết nối với nhau như thế nào?
Source code cần tối ưu ra sao?
Cần kiểm tra gì trước khi đưa website lên Internet?
Một dự án Laravel 13 hoàn chỉnh cần có những thành phần nào?
Đây cũng là bước chuyển từ:
"Học Laravel"
sang:
"Có thể tự xây dựng và triển khai một ứng dụng Laravel thực tế."
Trong suốt khóa học, chúng ta xây dựng một hệ thống Blog CMS có các chức năng chính:
BLOG CMS
│
┌──────────────┼──────────────┐
│ │ │
FRONTEND ADMIN API
│ │ │
▼ ▼ ▼
Blog list Dashboard REST API
Post detail Categories Sanctum
Category Posts Resources
Search Users Authentication
│ │ │
└──────────────┼──────────────┘
│
DATABASE
│
┌────────────┼────────────┐
▼ ▼ ▼
Users Categories Posts
Các thành phần chính gồm:
Authentication
Authorization
Users
Categories
Posts
Relationships
Validation
Upload Image
Search
Pagination
DataTables
Soft Delete
Events
Listeners
Queue
Mail
Notification
Cache
Storage
Logging
REST API
API Resource
Sanctum
Deployment
Nginx
Queue Worker
Scheduler
Production
Đây không còn là một ví dụ CRUD đơn giản.
Nó đã gần với cấu trúc của một ứng dụng web thực tế.
Chúng ta bắt đầu bằng những thành phần cơ bản:
Laravel
│
├── Composer
├── Artisan
├── MVC
├── Routing
├── Controller
└── Blade
Ví dụ một request:
Browser
│
▼
Route
│
▼
Controller
│
▼
Model
│
▼
Database
│
▼
Controller
│
▼
Blade
│
▼
HTML
│
▼
Browser
Đây là flow quan trọng nhất cần hiểu.
Chúng ta sử dụng Migration để quản lý cấu trúc database.
Ví dụ:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Thay vì tạo bảng thủ công bằng phpMyAdmin, cấu trúc database được lưu trong source code.
Điều này rất quan trọng khi làm việc nhóm hoặc triển khai lên server.
Seeder giúp tạo dữ liệu ban đầu:
php artisan db:seed
Factory giúp tạo dữ liệu giả:
Post::factory()->count(50)->create();
Nhờ đó chúng ta có thể nhanh chóng tạo:
100 Users
100 Categories
1000 Posts
để kiểm tra:
Pagination
Search
DataTables
Performance
Relationship
Eloquent là một trong những thành phần quan trọng nhất của Laravel.
Ví dụ:
$posts = Post::latest()->get();
Thay vì viết SQL trực tiếp:
SELECT *
FROM posts
ORDER BY created_at DESC;
Eloquent giúp code dễ đọc hơn.
Ví dụ:
$post = Post::find(1);
$post->update([
'title' => 'Laravel 13'
]);
Xóa:
$post->delete();
Blog CMS sử dụng nhiều Relationship.
Ví dụ:
User
│
└── hasMany()
│
▼
Posts
Category
│
└── hasMany()
│
▼
Posts
Và:
Post
│
├── belongsTo(User)
│
└── belongsTo(Category)
Trong Model:
public function user()
{
return $this->belongsTo(User::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
Sau đó:
$post->user->name;
$post->category->name;
Đây chính là sức mạnh của Eloquent Relationship.
Không nên tin dữ liệu từ người dùng.
Ví dụ:
$request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required'],
'category_id' => ['required', 'exists:categories,id'],
]);
Validation giúp đảm bảo dữ liệu trước khi đưa vào database.
Đối với upload:
'image' => [
'nullable',
'image',
'mimes:jpg,jpeg,png,webp',
'max:3072',
],
Validation phải được thực hiện ở server.
JavaScript phía client chỉ nên đóng vai trò hỗ trợ trải nghiệm người dùng.
Breeze giúp chúng ta xây dựng:
Register
Login
Logout
Password
Session
Authentication
Sau khi đăng nhập:
auth()->user()
có thể lấy thông tin user hiện tại.
Ví dụ:
$user = auth()->user();
echo $user->name;
Authentication trả lời:
Người này đã đăng nhập chưa?
Authorization trả lời:
Người này có quyền thực hiện hành động này không?
Ví dụ:
User
├── xem bài viết
└── không được quản lý user
Admin
├── xem bài viết
├── quản lý category
├── quản lý post
└── quản lý user
Đây là lý do chúng ta sử dụng:
Middleware
Policy
Gates
Role
Không nên chỉ ẩn nút bằng HTML.
Quyền phải được kiểm tra ở server.
Category là module CRUD đầu tiên của dự án.
Create
Read
Update
Delete
Ngoài ra còn:
Search
Pagination
Validation
Active / Inactive
Model có Scope:
#[Scope]
protected function active(Builder $query): void
{
$query->where('is_active', true);
}
Sau đó:
Category::active()->get();
Đây là một ví dụ về việc đưa logic truy vấn vào Model thay vì lặp lại trong Controller.
Post là module trung tâm của Blog CMS.
Một Post có:
title
slug
excerpt
content
image
status
published_at
view_count
user_id
category_id
Quan hệ:
User
│
└── Posts
Category
│
└── Posts
Post
├── User
└── Category
Controller xử lý:
Create
Edit
Update
Delete
Publish
Upload
Validation
Relationship
Phần frontend của Blog sử dụng:
BlogController
│
├── index()
├── show()
└── category()
Ví dụ danh sách bài viết:
Post::published()
->whereHas('category', fn ($query) => $query->active())
->with(['user', 'category'])
->latest('published_at')
->paginate(10);
Đây là một câu query đã kết hợp:
Local Scope
Relationship
Eager Loading
Sorting
Pagination
Một trong những lỗi performance phổ biến:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
}
Có thể dẫn tới rất nhiều query.
Thay vào đó:
$posts = Post::with('user')->get();
Hoặc:
$posts = Post::with([
'user',
'category'
])->get();
Nguyên tắc:
Khi biết trước sẽ sử dụng Relationship, hãy cân nhắc eager loading.
Không nên lấy toàn bộ database:
Post::all();
rồi xử lý bằng PHP khi dữ liệu lớn.
Thay vào đó:
Post::query()
->where('title', 'like', "%{$keyword}%")
->latest()
->paginate(10);
Database xử lý phần lớn công việc.
Ứng dụng chỉ nhận số lượng dữ liệu cần thiết.
Chúng ta đã thêm:
use SoftDeletes;
vào Model.
Khi:
$post->delete();
Post không nhất thiết bị xóa khỏi database.
Nó được đánh dấu bằng:
deleted_at
Có thể lấy lại:
$post->restore();
Xóa vĩnh viễn:
$post->forceDelete();
Đây là cơ chế rất hữu ích cho CMS.
Event giúp tách các hành động phụ khỏi logic chính.
Ví dụ:
User Register
│
▼
Registered Event
│
▼
Listener
│
├── Send Email
├── Log
└── Notification
Controller không nhất thiết phải xử lý tất cả mọi việc.
Một số công việc không cần thực hiện ngay trong HTTP request.
Ví dụ:
Gửi email
Xử lý ảnh
Tạo báo cáo
Gửi notification
Import dữ liệu
Có thể đưa vào Queue:
Request
│
▼
Dispatch Job
│
▼
Response ngay
│
│
▼
Queue Worker
│
▼
Process Job
Nhờ đó website phản hồi nhanh hơn.
Laravel cung cấp hệ thống:
Mail
Notification
cho nhiều hình thức thông báo.
Ví dụ:
User đăng ký
│
▼
Notification
│
├── Mail
├── Database
└── các channel khác
Trong dự án thực tế, các tác vụ gửi mail số lượng lớn nên được kết hợp với Queue.
Cache dùng để giảm số lần truy vấn hoặc xử lý dữ liệu.
Ví dụ:
$categories = Cache::remember(
'blog.categories',
3600,
fn () => Category::active()->get()
);
Flow:
Request
│
▼
Cache?
┌─┴─┐
Có Không
│ │
▼ ▼
Data Database
│
▼
Cache
Nhưng cache phải có chiến lược invalidation.
Ví dụ khi Category thay đổi:
Cache::forget('blog.categories');
Không nên cache mọi thứ một cách máy móc.
File upload nên được quản lý thông qua Laravel Storage.
Ví dụ:
$path = $request->file('image')
->store('posts', 'public');
File có thể được lưu trong disk:
storage/app/public
và tạo symbolic link:
php artisan storage:link
Không nên tự xây dựng hệ thống upload bằng cách xử lý đường dẫn file một cách tùy tiện.
Khi production, log rất quan trọng.
Ví dụ:
Log::info('Post published', [
'post_id' => $post->id,
'user_id' => auth()->id(),
]);
Khi xảy ra lỗi:
Log::error('Post publish failed', [
'post_id' => $post->id,
]);
Development có thể sử dụng:
dd($data);
hoặc:
dump($data);
Nhưng production phải tránh để debug information xuất hiện cho người dùng.
Laravel 13 cũng cung cấp health route /up, hữu ích cho monitoring và kiểm tra ứng dụng đã boot thành công hay chưa.
Blog CMS của chúng ta không chỉ có giao diện HTML.
Chúng ta còn xây dựng API:
GET
POST
PUT / PATCH
DELETE
Ví dụ:
GET /api/posts
GET /api/posts/{id}
POST /api/posts
PUT /api/posts/{id}
DELETE /api/posts/{id}
API trả về JSON thay vì HTML.
Không nên trả trực tiếp toàn bộ Model:
return $post;
Đối với API thực tế, nên kiểm soát dữ liệu trả về bằng:
PostResource
Ví dụ:
return new PostResource($post);
Danh sách:
return PostResource::collection($posts);
Resource giúp API có cấu trúc rõ ràng và tránh vô tình trả những dữ liệu không nên public.
Sanctum cung cấp cơ chế xác thực cho API.
Flow:
Client
│
│ email + password
▼
API Login
│
▼
Sanctum
│
▼
Token
│
▼
Client
Các request tiếp theo:
Authorization: Bearer TOKEN
Route private:
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
});
Một API thực tế cần có:
Register
Login
Logout
Current User
Token
Validation
Authorization
Và phải phân biệt:
401 Unauthorized
với:
403 Forbidden
Hiểu đơn giản:
401
→ Chưa xác thực / token không hợp lệ
403
→ Đã xác thực nhưng không có quyền
Đây là nền tảng để xây dựng API cho:
Mobile App
SPA
Frontend JavaScript
Third-party integrations
Sau khi hoàn thành source code, ứng dụng phải được đưa lên server.
Kiến trúc phổ biến:
Internet
│
▼
Domain
│
▼
Nginx
│
▼
PHP-FPM
│
▼
Laravel 13
│
├── MySQL
├── Redis / Cache
├── Storage
└── Queue
Laravel 13 yêu cầu PHP tối thiểu 8.3. Khi dùng Nginx, web server phải trỏ vào thư mục public, không phải root của project.
Một production .env cơ bản có thể có:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
Database:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=blog_user
DB_PASSWORD=********
Mail:
MAIL_MAILER=smtp
MAIL_HOST=...
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_ENCRYPTION=tls
Không commit .env vào Git.
Đặc biệt:
APP_KEY
DB_PASSWORD
MAIL_PASSWORD
API_KEY
TOKEN
đều phải được bảo vệ.
Laravel cung cấp:
php artisan optimize
để cache các thành phần phù hợp cho production.
Có thể xóa cache bằng:
php artisan optimize:clear
Laravel 13 cũng cung cấp các lệnh cache riêng cho:
php artisan config:cache
php artisan event:cache
php artisan route:cache
php artisan view:cache
Các cache này nên được thực hiện trong quy trình deployment, không phải tùy tiện trong lúc development.
Khi deploy:
composer install --no-dev --optimize-autoloader
Không cần mang theo các package chỉ phục vụ development nếu chúng không cần thiết trên production.
Sau đó:
php artisan optimize
Nếu project sử dụng Vite:
npm install
Sau đó build production:
npm run build
Không nên sử dụng development server để phục vụ production.
Production migration:
php artisan migrate --force
--force cho phép migration chạy trong môi trường production mà không yêu cầu xác nhận tương tác.
Trước migration quan trọng:
Backup Database
│
▼
Run Migration
│
▼
Check Application
│
▼
Monitor Logs
Đừng biến production database thành nơi thử nghiệm.
Sau deployment:
php artisan storage:link
Laravel cần quyền ghi vào:
storage/
bootstrap/cache/
Đây là những thư mục quan trọng đối với hoạt động runtime và cache của ứng dụng.
Nếu Blog CMS sử dụng Queue, production phải có worker chạy liên tục.
Ví dụ:
php artisan queue:work
Không nên chỉ chạy thủ công trong terminal rồi đóng terminal.
Production thường cần:
Supervisor
hoặc
systemd
hoặc
một process manager tương đương
để worker tự khởi động lại khi gặp lỗi.
Sau khi deploy code mới, các process chạy lâu như queue worker cần được reload/restart để sử dụng code mới. Laravel 13 cung cấp:
php artisan reload
cho mục đích này.
Nếu project có scheduled tasks, server cần gọi:
php artisan schedule:run
thường xuyên theo cơ chế scheduler của Laravel.
Ví dụ:
00:00
│
▼
Scheduler
│
├── Clean old logs
├── Send reports
├── Publish scheduled posts
└── Cleanup temporary files
Đây là phần rất dễ bị quên khi chuyển từ localhost lên VPS.
Trước khi đưa Blog CMS lên Internet:
[ ] APP_ENV=production
[ ] APP_DEBUG=false
[ ] APP_URL chính xác
[ ] APP_KEY tồn tại
[ ] Database production chính xác
[ ] Database đã backup
[ ] .env không commit Git
[ ] composer install --no-dev
[ ] npm run build
[ ] migrate --force
[ ] storage:link
[ ] storage permission
[ ] bootstrap/cache permission
[ ] php artisan optimize
[ ] Nginx root = public/
[ ] PHP-FPM hoạt động
[ ] HTTPS hoạt động
[ ] Queue Worker hoạt động
[ ] Scheduler hoạt động
[ ] Mail hoạt động
[ ] Upload hoạt động
[ ] Login hoạt động
[ ] Authorization hoạt động
[ ] API hoạt động
[ ] /up hoạt động
[ ] Logs hoạt động
Một Blog CMS production tối thiểu cần kiểm tra:
[ ] APP_DEBUG=false
[ ] Không public .env
[ ] HTTPS
[ ] Password được hash
[ ] Validation
[ ] Authorization
[ ] CSRF protection
[ ] Mass Assignment protection
[ ] SQL Injection protection
[ ] XSS protection
[ ] Upload validation
[ ] File permission
[ ] Rate limiting nếu cần
[ ] API authentication
[ ] Token được bảo vệ
[ ] Database backup
[ ] Server update thường xuyên
Đặc biệt:
Đừng nhầm việc ẩn một nút trên giao diện với việc bảo vệ quyền truy cập.
Ví dụ:
@if(auth()->user()->role === 'admin')
<a href="/admin/users">
Users
</a>
@endif
chỉ là UI.
Controller hoặc Policy vẫn phải kiểm tra quyền.
Khi project lớn dần, database thường trở thành một trong những điểm cần quan tâm đầu tiên.
Không nên:
Post::all();
nếu database có hàng triệu record.
Nên:
Post::latest()->paginate(20);
Không nên lấy toàn bộ column nếu chỉ cần một số field:
Post::select([
'id',
'title',
'slug',
])->get();
Sử dụng index cho những column thường xuyên:
WHERE
ORDER BY
JOIN
UNIQUE
Ví dụ:
$table->string('slug')->unique();
đã tạo unique index cho slug.
Một source code tốt không phải source code có nhiều file nhất.
Mục tiêu là:
Dễ đọc
Dễ sửa
Dễ test
Dễ debug
Dễ deploy
Dễ mở rộng
Controller không nên trở thành:
God Controller
Ví dụ không nên để một Controller xử lý:
Database
Email
Upload
Payment
Notification
Report
API
tất cả trong một method khổng lồ.
Hãy chia trách nhiệm hợp lý.
Khi thêm một tính năng mới, có thể đi theo flow:
Requirement
│
▼
Database?
│
▼
Migration
│
▼
Model
│
▼
Relationship
│
▼
Request / Validation
│
▼
Controller
│
▼
Route
│
▼
View / API
│
▼
Authorization
│
▼
Test
│
▼
Deploy
Đây là workflow rất đáng ghi nhớ.
Một workflow đơn giản:
Local Development
│
▼
Git
│
▼
Git Repository
│
▼
VPS
│
▼
composer install
│
▼
npm run build
│
▼
migrate
│
▼
optimize
│
▼
reload services
Ví dụ:
git add .
git commit -m "Update blog CMS"
git push
Trên server:
git pull
composer install --no-dev --optimize-autoloader
npm install
npm run build
php artisan migrate --force
php artisan optimize
php artisan reload
Tất nhiên quy trình thực tế có thể khác tùy hạ tầng.
Một cấu trúc khái quát:
blog/
│
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ ├── Middleware/
│ │ ├── Requests/
│ │ └── Resources/
│ │
│ ├── Models/
│ ├── Policies/
│ ├── Events/
│ ├── Listeners/
│ ├── Jobs/
│ ├── Mail/
│ └── Notifications/
│
├── bootstrap/
│
├── config/
│
├── database/
│ ├── factories/
│ ├── migrations/
│ └── seeders/
│
├── public/
│
├── resources/
│ ├── css/
│ ├── js/
│ └── views/
│
├── routes/
│ ├── web.php
│ └── api.php
│
├── storage/
│
├── tests/
│
├── .env
├── artisan
├── composer.json
└── package.json
Cấu trúc Laravel có thể được tổ chức thêm tùy dự án, nhưng các thư mục cốt lõi được framework cung cấp nhằm tạo nền tảng cho cả ứng dụng nhỏ và lớn.
Có thể tóm tắt khóa Laravel 13 thành:
PHP
│
▼
Laravel
│
├── Routing
│
├── Controller
│
├── Blade
│
├── Database
│ ├── Migration
│ ├── Seeder
│ └── Factory
│
├── Eloquent
│ ├── CRUD
│ ├── Query
│ └── Relationship
│
├── Form
│ ├── Request
│ ├── Validation
│ └── Upload
│
├── Authentication
│ ├── Login
│ ├── Register
│ ├── Middleware
│ └── Authorization
│
├── Advanced
│ ├── Scope
│ ├── Observer
│ ├── Event
│ ├── Queue
│ ├── Mail
│ ├── Notification
│ ├── Cache
│ ├── Storage
│ └── Logging
│
├── REST API
│ ├── Resource
│ ├── Sanctum
│ └── Authentication
│
└── Production
├── VPS
├── Nginx
├── Queue Worker
├── Scheduler
└── Optimization
Đó chính là toàn bộ hành trình của khóa học.
Laravel 13 tiếp tục chu kỳ phát hành hàng năm của Laravel.
Theo tài liệu chính thức, Laravel 13 yêu cầu PHP 8.3–8.5; Laravel 13 được phát hành ngày 17/03/2026, với bug fixes đến Q3/2027 và security fixes đến 17/03/2028. Laravel 13 không phải bản LTS.
Vì vậy, nếu xây dựng khóa học hiện tại theo Laravel 13 thì nên ghi:
Laravel 13.x — 2026
thay vì:
Laravel 12 LTS
Sau khi hoàn thành khóa học, người học không chỉ biết:
Route::get(...);
hay:
Post::all();
Mà đã hiểu được cách một ứng dụng web thực tế được xây dựng:
User
│
▼
Browser
│
▼
Nginx
│
▼
Laravel
│
├── Middleware
├── Route
├── Controller
├── Validation
├── Model
├── Database
├── Cache
├── Queue
└── Storage
│
▼
Response
Đây mới là mục tiêu chính của khóa học.
Sau Blog CMS, có thể tiếp tục theo nhiều hướng.
Laravel
↓
REST API
↓
Redis
↓
Queue
↓
WebSocket
↓
Microservices
Laravel API
↓
Vue
React
Livewire
Linux
↓
Nginx
↓
Docker
↓
CI/CD
↓
Cloud
MySQL
↓
Index
↓
Query Optimization
↓
Redis
↓
Database Scaling
PHPUnit
↓
Feature Test
↓
API Test
↓
Integration Test
Phần Bonus của khóa học chỉ cần tập trung vào những khái niệm cơ bản:
Test
Feature Test
API Test
assertOk()
assertStatus()
assertJson()
Ví dụ:
$response = $this->get('/blog');
$response->assertOk();
API:
$response = $this->getJson('/api/posts');
$response->assertStatus(200);
Kiểm tra JSON:
$response->assertJson([
'data' => [],
]);
Mục tiêu của phần Bonus không phải biến người học thành chuyên gia testing.
Mục tiêu là giúp người học hiểu:
Code có thể được kiểm tra tự động thay vì chỉ test bằng tay.
Đây là danh sách chức năng hoàn chỉnh:
BLOG CMS
│
├── Authentication
│ ├── Register
│ ├── Login
│ └── Logout
│
├── Dashboard
│
├── Categories
│ ├── Create
│ ├── Read
│ ├── Update
│ ├── Delete
│ ├── Search
│ └── Pagination
│
├── Posts
│ ├── Create
│ ├── Read
│ ├── Update
│ ├── Delete
│ ├── Publish
│ ├── Upload Image
│ ├── Search
│ └── Pagination
│
├── Users
│ ├── List
│ ├── Role
│ ├── Active / Inactive
│ └── Password
│
├── Soft Delete
│
├── Roles & Permissions
│
├── Events
│
├── Queue
│
├── Mail
│
├── Notification
│
├── Cache
│
├── Storage
│
├── Logging
│
├── REST API
│
├── API Resource
│
├── Sanctum
│
└── Deployment
├── Shared Hosting
├── VPS
├── Nginx
├── Queue Worker
└── Scheduler
Trước khi gọi project là hoàn thành:
[✓] Register
[✓] Login
[✓] Logout
[✓] Categories
[✓] Posts
[✓] Users
[✓] Upload
[✓] Search
[✓] Pagination
[✓] Soft Delete
[✓] Roles
[✓] API
[✓] Sanctum
[✓] Validation
[✓] Relationship
[✓] Scope
[✓] Authorization
[✓] Error handling
[✓] Logging
[✓] Cache
[✓] Queue
[✓] .env
[✓] APP_DEBUG=false
[✓] HTTPS
[✓] Database
[✓] Storage
[✓] Nginx
[✓] PHP-FPM
[✓] Queue Worker
[✓] Scheduler
[✓] Backup
[✓] Monitoring
Đây là điểm quan trọng nhất của toàn bộ khóa học.
Học Laravel không nên dừng ở:
Biết Route
Biết Controller
Biết Model
Biết Blade
Mà phải tiến đến:
Nhận Requirement
↓
Thiết kế Database
↓
Thiết kế Architecture
↓
Viết Code
↓
Validation
↓
Authorization
↓
Testing
↓
Debug
↓
Optimization
↓
Deployment
↓
Monitoring
Đó mới là quy trình làm một ứng dụng thực tế.
Chúng ta bắt đầu từ:
PHP
sau đó:
Laravel
rồi:
Database
tiếp tục:
Eloquent
sau đó:
Authentication
rồi:
CRUD
tiếp tục:
API
và cuối cùng:
Production
Toàn bộ hành trình có thể tóm tắt bằng một dòng:
PHP
↓
Laravel 13
↓
Blog CMS
↓
REST API
↓
Authentication
↓
Queue / Cache / Storage
↓
VPS / Nginx
↓
Production
Nếu đã hoàn thành toàn bộ dự án Blog CMS, người học đã có một nền tảng đủ để bắt đầu xây dựng những ứng dụng Laravel lớn hơn.
Điều quan trọng từ đây không còn là:
"Laravel có bao nhiêu tính năng?"
mà là:
"Tôi có thể dùng Laravel để giải quyết bài toán gì?"
Framework chỉ là công cụ.
Kỹ năng quan trọng hơn là khả năng:
Phân tích vấn đề
↓
Thiết kế giải pháp
↓
Viết code
↓
Kiểm tra
↓
Tối ưu
↓
Triển khai
↓
Bảo trì
Và đó cũng chính là mục tiêu cuối cùng của khóa học:
┌─────────────────────────────────────────┐
│ │
│ PHP → LARAVEL 13 → CMS │
│ │
│ 50 BÀI HỌC + BONUS PHPUnit │
│ │
│ Database │
│ Eloquent │
│ Authentication │
│ CRUD │
│ API │
│ Sanctum │
│ Queue │
│ Cache │
│ Storage │
│ Deployment │
│ │
│ BLOG CMS │
│ │
└─────────────────────────────────────────┘
Hết phần Laravel 13.
Bước tiếp theo không phải học thêm thật nhiều syntax.
Hãy lấy Blog CMS này làm nền tảng và bắt đầu xây dựng project của riêng mình.
x1
Cập nhật: 2026-09-29T02:30:03.955+07:00
Trong 50 bài chính, chúng ta đã hoàn thành một Blog CMS với:
Authentication
CRUD
Database
Eloquent
Relationship
Validation
Upload
Search
Pagination
Soft Delete
Authorization
Queue
Notification
Cache
Storage
Logging
REST API
Sanctum
Deployment
Nhưng có một vấn đề:
Làm sao biết code của chúng ta vẫn hoạt động đúng sau khi sửa hoặc thêm tính năng mới?
Đó là lúc Automated Testing xuất hiện.
Trong bài Bonus này, chúng ta chỉ học những kiến thức PHPUnit cần thiết để bắt đầu kiểm thử một ứng dụng Laravel 13.
p/S: mục 8 trở đi mới chạy được.
Testing là quá trình kiểm tra xem chương trình có hoạt động đúng theo yêu cầu hay không.
Ví dụ Blog CMS có chức năng:
GET /blog
Chúng ta mong muốn:
HTTP 200
Thay vì mỗi lần sửa code lại mở trình duyệt kiểm tra bằng tay, chúng ta có thể viết test:
$response = $this->get('/blog');
$response->assertOk();
Sau đó Laravel tự kiểm tra.
Nếu đúng:
PASS
Nếu sai:
FAIL
PHPUnit là framework testing phổ biến trong PHP.
Laravel tích hợp sẵn khả năng testing với PHPUnit và Pest. Một project Laravel mới đã có phpunit.xml và thư mục tests.
Trong bài này chúng ta tập trung vào:
PHPUnit
vì mục tiêu của Bonus là giúp người mới hiểu nền tảng testing.
Trong Laravel:
tests/
│
├── Feature/
│
├── Unit/
│
└── TestCase.php
Hai loại quan trọng:
Unit Test
Feature Test
Unit Test kiểm tra một phần nhỏ và tương đối độc lập của code.
Ví dụ:
Một method
Một class
Một hàm xử lý
Một business rule
Ví dụ:
public function add(int $a, int $b): int
{
return $a + $b;
}
Test:
$this->assertEquals(
5,
$calculator->add(2, 3)
);
Unit Test thường không cần boot toàn bộ ứng dụng Laravel hoặc truy cập database.
Feature Test kiểm tra một chức năng lớn hơn.
Ví dụ:
Browser
↓
Route
↓
Middleware
↓
Controller
↓
Model
↓
Database
↓
Response
Ví dụ:
$response = $this->get('/blog');
$response->assertOk();
Đây là một Feature Test.
Laravel lưu ý rằng Feature Test có thể kiểm tra cả HTTP request, database và nhiều thành phần phối hợp với nhau; trong ứng dụng thực tế, phần lớn test thường có thể tập trung vào Feature Test vì nó cho mức độ tin cậy cao hơn về hành vi tổng thể của hệ thống.
Laravel cung cấp Artisan command:
php artisan make:test BlogTest
File sẽ nằm trong:
tests/Feature/BlogTest.php
Nếu muốn tạo Unit Test:
php artisan make:test CalculatorTest --unit
File:
tests/Unit/CalculatorTest.php
Tạo:
php artisan make:test BlogTest
Sau đó mở:
tests/Feature/BlogTest.php
Ví dụ:
<?php
// php artisan make:test BlogTest
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
//php artisan test --filter=BlogTest
class BlogTest extends TestCase
{
use RefreshDatabase;
// Tự động nạp dữ liệu mẫu từ DatabaseSeeder vào database testing
protected $seed = true;
public function test_blog_page_is_accessible(): void
{
$response = $this->get('/blog');
$response->assertOk();
}
}
Test này nói:
Khi request
/blog, ứng dụng phải trả HTTP 200.
Trước khi chạy Test phải vào file Pest.php comment dòng này lại nếu không sẽ kích hoạt refresh database mất hết dữ liệu.
pest()->extend(TestCase::class)
// ->use(RefreshDatabase::class) // Comment dòng này lại
->in('Feature');
Tóm lại:&
dùng file .env.testing ngay từ đầu + thêm biến seed để laravel lấy dữ liệu mẫu tránh chung đụng dữ liệu testing và dữ liệu thật.
Xem mục&17. Test Post
Có thể chạy:
php artisan test
Laravel cũng cho phép chạy trực tiếp PHPUnit:
vendor/bin/phpunit
Laravel cung cấp php artisan test như một cách thuận tiện để chạy test suite.
Kết quả có thể giống:
PASS Tests\Feature\BlogTest
✓ blog page is accessible
Tests: 1 passed
Nếu test thất bại:
FAIL
Laravel sẽ hiển thị thông tin để chúng ta tìm nguyên nhân.
Ghi chú:
1.lệnh chạy trực tiếp phpunit ngoài thư mục gốc phải gõ dấu \ mới chạy
vendor\bin\phpunit tests\Feature\BlogTest.php
2.nên chạy riêng BlogTest.php để tránh hiển thị quá nhiều
php artisan test tests/Feature/BlogTest.phpMột assertion rất thường dùng:
$response->assertOk();
Nó kiểm tra HTTP response có status:
200 OK
Ví dụ:
public function test_blog_page_is_accessible(): void
{
$response = $this->get('/blog');
$response->assertOk();
}
Có thể kiểm tra status cụ thể:
$response->assertStatus(200);
Ví dụ:
$response = $this->get('/blog');
$response->assertStatus(200);
Hoặc:
$response->assertStatus(404);
Ví dụ test một trang không tồn tại:
public function test_missing_page_returns_404(): void
{
$response = $this->get('/this-page-does-not-exist');
$response->assertStatus(404);
}
Nếu request phải redirect:
$response->assertRedirect('/login');
Ví dụ:
public function test_admin_page_requires_login(): void
{
$response = $this->get('/admin/users');
$response->assertRedirect('/login');
}
Đây là một test rất thực tế đối với Blog CMS.
Có thể kiểm tra response có chứa text:
$response->assertSee('Blog');
Ví dụ:
public function test_blog_contains_title(): void
{
$response = $this->get('/blog');
$response->assertOk();
$response->assertSee('Blog');
}
Hoặc:
$response->assertDontSee('Something');
Đây là phần đặc biệt quan trọng đối với Blog CMS.
API:
GET /api/posts
Test:
public function test_posts_api_returns_success(): void
{
$response = $this->getJson('/api/posts');
$response->assertStatus(200);
}
Hoặc:
$response->assertOk();
Giả sử API trả về:
{
"data": [
{
"id": 1,
"title": "Laravel 13"
}
]
}
Có thể kiểm tra:
$response->assertJson([
'data' => [
[
'id' => 1,
'title' => 'Laravel 13',
],
],
]);
Mục tiêu là kiểm tra API trả về đúng cấu trúc dữ liệu mong muốn.
Ghi chú: ví dụ trên chỉ đúng nếu trả về api/posts/1
Đối với trường hợp trả về nguyên mảng phải viết lại:
// test API danh sách posts
public function test_posts_api_returns_success(): void
{
$response = $this->getJson('/api/posts');
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'*' => [ // <--- Dấu * đại diện cho từng phần tử trong mảng data
'id',
'title',
'slug',
'excerpt',
'image_url',
'published_at',
'is_published',
'views',
'author' => ['id', 'name', 'email', 'role', 'created_at'],
'category' => ['id', 'name', 'slug']
]
]
]);
}
Feature Test có thể kiểm tra database.
Laravel cung cấp các trait hỗ trợ việc reset database giữa các test.
Ví dụ:
use Illuminate\Foundation\Testing\RefreshDatabase;
Sau đó:
class PostTest extends TestCase
{
use RefreshDatabase;
// ...
}
Mỗi test có thể làm việc với database testing thay vì phá dữ liệu development hiện tại.
Giả sử chúng ta có:
PostFactory
Có thể tạo Post:
$post = Post::factory()->create();
Sau đó test:
$response = $this->get('/blog/' . $post->slug);
$response->assertOk();
Đây là cách rất phù hợp với Blog CMS.
Trước khi chạy ví dụ dưới để tránh mất data phải:
.env.testing và kiểm tra xem laravel đang nhận database nào lúc chạy testphp artisan config:clear
php artisan env --env=testing
php artisan config:show database.default --env=testingNội dung file .env.testing
APP_ENV=testing
APP_KEY=base64:key
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_db_test
DB_USERNAME=root
DB_PASSWORD=
# Bắt buộc xóa hẳn hoặc để trống dòng DB_URL
DB_URL=Nhớ vào phpmyadmin tạo database có tên là: your_db_test
database này sẽ trống trơn do phpunit.xml cấu hình chạy trên RAM.
Data lúc này dựa vào Factory() hoặc $seed (tạo toàn bộ db -> khá nặng) như trường hợp BlogTest
Phải sửa trong PostFactory
// Khai báo mối quan hệ theo chuẩn Laravel
'user_id' => User::factory(),
'category_id' => Category::factory(),Ví dụ:
<?php
namespace Tests\Feature;
use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_post_page_can_be_opened(): void
{
$user = User::factory()->create();
$category = Category::factory()->create();
// Ép status = published để route tìm thấy bài viết
$post = Post::factory()->create([
'user_id' => $user->id,
'category_id' => $category->id,
'status' => 'published',
'published_at' => now(),
]);
$response = $this->get(
'/blog/' . $post->slug
);
$response->assertOk();
}
}
Flow:
Factory
↓
Create Post
↓
Request URL
↓
Laravel
↓
Response
↓
assertOk()
Ví dụ một trang admin yêu cầu đăng nhập.
public function test_admin_requires_authentication(): void
{
$response = $this->get('/admin/users');
$response->assertRedirect('/login');
}
Sau đó test user đã đăng nhập (đôi khi user thường k cho vào phải role admin):
//$user = User::factory()->create();
$user = User::factory()->create(['role' => 'admin']);
$this->actingAs($user);
$response = $this->get('/admin/users');
// Đã đăng nhập -> Phải trả về 200 OK thành công
$response->assertOk();
Đây là cách kiểm tra các route có authentication.
Authentication chưa đủ.
Chúng ta còn phải kiểm tra quyền.
Ví dụ:
user
admin
Một user thông thường không được phép truy cập:
/admin/users
Test có thể kiểm tra:
$user = User::factory()->create([
'role' => 'user',
]);
$this->actingAs($user);
$response = $this->get('/admin/users');
$response->assertForbidden();
Trong trường hợp ứng dụng của bạn trả về redirect hoặc response khác tùy middleware/policy, assertion phải phù hợp với implementation thực tế.
Điểm quan trọng:
Phải test cả Authentication và Authorization.
Ví dụ tạo Post yêu cầu:
title
content
category_id
Nếu gửi request thiếu title:
$response = $this->post('/admin/posts', [
'content' => 'Test content',
]);
Có thể kiểm tra validation:
$response->assertSessionHasErrors([
'title',
]);
Đây là một test rất hữu ích.
Full code:
/**
* Test validation khi tạo bài viết mới thiếu các trường bắt buộc.
*/
public function test_create_post_validation_fails_when_required_fields_are_missing(): void
{
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin);
// Gửi request rỗng
$response = $this->post('/admin/posts', []);
// Kiểm tra đúng 4 trường bắt buộc (có chữ required)
$response->assertSessionHasErrors([
'title',
'slug',
'content',
'status',
]);
$response->assertStatus(302);
}Blog CMS của chúng ta có:
GET
POST
PUT/PATCH
DELETE
Do đó có thể xây dựng một bộ test:
Post API
│
├── GET /api/posts
├── GET /api/posts/{id}
├── POST /api/posts
├── PUT /api/posts/{id}
└── DELETE /api/posts/{id}
Ví dụ:
public function test_posts_api_returns_json(): void
{
$response = $this->getJson('/api/posts');
$response->assertOk();
$response->assertJsonStructure([
'data',
]);
}
API private yêu cầu:
Bearer Token
Có thể tạo user:
$user = User::factory()->create();
Tạo token:
$token = $user->createToken('testing')->plainTextToken;
Sau đó request:
$response = $this
->withToken($token)
->getJson('/api/user');
Kiểm tra:
$response->assertOk();
Test request không có token:
public function test_private_api_requires_authentication(): void
{
$response = $this->getJson('/api/user');
$response->assertUnauthorized();
}
Kết quả mong muốn:
HTTP 401
Sau đó test với token hợp lệ:
$user = User::factory()->create();
$token = $user
->createToken('testing')
->plainTextToken;
$response = $this
->withToken($token)
->getJson('/api/user');
$response->assertOk();
Trong Blog CMS, chúng ta đã học Event và Listener.
Testing cũng có thể kiểm tra Event có được dispatch hay không.
Trước tiên vào app/Events/ xem có event nào hoặc dùng lệnh:
php artisan event:listVí dụ: PostPublished và UserRegistered (đã học trong bài Event)
Sau đó vào PostController xem hàm store dòng:
event(new PostPublished($post));Laravel cung cấp:
Event::fake();
Sau đó chạy code cần kiểm tra.
Cuối cùng:
Event::assertDispatched(
Registered::class
);
Laravel hỗ trợ Event::fake() và các assertion như assertDispatched() để kiểm tra event mà không cần thực thi listener trong test đó.
Flow:
Test
│
▼
Event::fake()
│
▼
Execute Code
│
▼
Event dispatched?
│
├── YES → PASS
└── NO → FAIL
Dưới đây là full code passed
use App\Events\PostPublished;
use App\Events\UserRegistered;
use App\Models\Post;
use App\Models\User;
use App\Models\Category;
use Illuminate\Support\Facades\Event;
function test_user_registered_event_is_dispatched(): void
{
Event::fake();
$user = User::factory()->create();
event(new UserRegistered($user));
Event::assertDispatched(
UserRegistered::class
);
}
//test event published_post bắn ra khi store post thành công chọn published
public function test_creating_published_post_dispatches_event(): void
{
Event::fake();
$admin = User::factory()->create([
'role' => 'admin',
]);
$category = Category::factory()->create();
$this->actingAs($admin);
$response = $this->post('/admin/posts', [
'category_id' => $category->id,
'title' => 'Laravel Testing',
'slug' => 'laravel-testing',
'content' => str_repeat('Nội dung test ', 5),
'status' => 'published',
]);
Event::assertDispatched(
PostPublished::class,
fn (PostPublished $event)
=> $event->post->title === 'Laravel Testing'
);
}
//test event published_post k bắn ra khi draft
public function test_creating_draft_post_does_not_dispatch_event(): void
{
Event::fake();
$admin = User::factory()->create([
'role' => 'admin',
]);
$category = Category::factory()->create();
$this->actingAs($admin);
$this->post('/admin/posts', [
'category_id' => $category->id,
'title' => 'Draft Post',
'slug' => 'draft-post',
'content' => str_repeat('Nội dung test ', 5),
'status' => 'draft',
]);
Event::assertNotDispatched(
PostPublished::class
);
}
Tương tự Event, Queue cũng có thể được fake.
Ví dụ:
Queue::fake();
Sau đó:
SomeJob::dispatch();
Kiểm tra:
Queue::assertPushed(SomeJob::class);
Mục tiêu:
Kiểm tra Job đã được dispatch hay chưa.
Không nhất thiết phải thực sự chạy Job trong test này.
use App\Jobs\SendWelcomeEmail;
use App\Models\User;
use Illuminate\Support\Facades\Queue;
public function test_welcome_email_job_is_queued(): void
{
Queue::fake();
$user = User::factory()->create();
SendWelcomeEmail::dispatch($user);
Queue::assertPushed(
SendWelcomeEmail::class
);
}Mail cũng có thể fake.
Ví dụ:
Mail::fake();
Sau đó:
Mail::to($user)->send(
new WelcomeMail($user)
);
Kiểm tra:
Mail::assertSent(WelcomeMail::class);
Điều này đặc biệt hữu ích với:
Register
Password Reset
Contact Form
Notification
Order Confirmation
Full Code MailTest:
//php artisan make:test MailTest
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeUser;
//php artisan test tests/Feature/MailTest.php
class MailTest extends TestCase
{
public function test_user_receives_welcome_email()
{
Mail::fake();
Mail::to('test@example.com')
->send(new WelcomeUser('Nguyễn Văn A'));
Mail::assertSent(WelcomeUser::class, function ($mail) {
return $mail->hasTo('test@example.com')
&& $mail->name === 'Nguyễn Văn A';
});
}
}
//END class
Blog CMS có upload image.
Có thể fake Storage:
Storage::fake('public');
Sau đó:
$file = UploadedFile::fake()
->image('post.jpg');
Gửi request:
$response = $this->post('/admin/posts', [
'title' => 'Laravel 13',
'image' => $file,
]);
Kiểm tra file:
Storage::disk('public')->assertExists(
'posts/' . $file->hashName()
);
Đây là cách test upload mà không cần ghi file thật vào storage production.
Full code UploadTest:
// php artisan make:test UploadTest
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use App\Models\User;
use App\Models\Country;
// php artisan test tests/Feature/UploadTest.php
class UploadTest extends TestCase
{
use RefreshDatabase;
public function test_image_can_be_uploaded()
{
Storage::fake('public');
$user = User::factory()->create([
'role' => 'admin',
]);
$file = UploadedFile::fake()
->image('post.jpg');
$response = $this->actingAs($user)
->post('/admin/posts', [
'title' => 'Laravel 13',
'slug' => 'laravel-13',
'content' => str_repeat('Nội dung test ', 5),
'status' => 'draft',
'image' => $file,
]);
$response->assertRedirect();
Storage::disk('public')->assertExists(
'posts/' . $file->hashName()
);
}
}
//END classBlog CMS có Soft Delete.
Có thể kiểm tra:
$post = Post::factory()->create();
$post->delete();
Sau đó:
$this->assertSoftDeleted($post);
Hoặc kiểm tra record đã bị đánh dấu:
deleted_at != NULL
Sau đó test restore:
$post->restore();
Full code SoftDeleteTest:
// php artisan make:test SoftDeleteTest
namespace Tests\Feature;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
// php artisan test tests/Feature/SoftDeleteTest.php
class SoftDeleteTest extends TestCase
{
use RefreshDatabase;
public function test_post_can_be_soft_deleted()
{
$post = Post::factory()->create();
$post->delete();
$this->assertSoftDeleted($post);
}
public function test_deleted_at_is_not_null_after_delete()
{
$post = Post::factory()->create();
$post->delete();
$this->assertNotNull(
$post->fresh()->deleted_at
);
}
public function test_post_can_be_restored()
{
$post = Post::factory()->create();
$post->delete();
$this->assertSoftDeleted($post);
$post->restore();
$this->assertNull(
$post->fresh()->deleted_at
);
}
}
Ví dụ Post:
Create
Read
Update
Delete
Chúng ta có thể xây dựng:
PostTest
│
├── can list posts
├── can show post
├── can create post
├── can update post
├── can delete post
└── can restore post
Đây chính là cách biến test thành một bộ kiểm tra tự động cho CMS.
<?php
// php artisan make:test CRUDPostTest
namespace Tests\Feature;
use App\Models\Post;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
// php artisan test --filter=CRUDPostTest
class CRUDPostTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected function setUp(): void
{
parent::setUp();
// Tạo Admin User và authenticate toàn bộ request trong class
$this->user = User::factory()->create([
'role' => 'admin',
]);
$this->actingAs($this->user);
}
/**
* Test 1: Hiển thị danh sách bài viết (Index)
*/
public function test_can_list_posts(): void
{
Post::factory()->count(5)->create([
'status' => 'published',
]);
$response = $this->get(route('admin.posts.index'));
$response->assertStatus(200);
}
/**
* Test 2: Xem chi tiết bài viết (Show)
*/
public function test_can_show_post(): void
{
$post = Post::factory()->create([
'title' => 'Bài viết thử nghiệm',
'slug' => 'bai-viet-thu-nghiem',
'content' => 'Nội dung chi tiết bài viết',
'status' => 'published',
]);
$response = $this->get(route('admin.posts.show', $post));
$response->assertStatus(200);
}
/**
* Test 3: Tạo mới bài viết (Store)
*/
public function test_can_create_post(): void
{
// Đảm bảo gửi ĐỦ dữ liệu theo đúng validation của validatePost()
$payload = [
'title' => 'Tựa đề bài viết mới',
'slug' => 'tua-de-bai-viet-moi-test',
'content' => 'Nội dung bài viết tạo từ test case',
'status' => 'draft',
'user_id' => $this->user->id,
'image' => UploadedFile::fake()->image('post.jpg'), // Giả lập file ảnh thật
];
$response = $this->post(route('admin.posts.store'), $payload);
// Bắt buộc check không dính lỗi Validation
//$response->assertSessionHasNoErrors();
$response->assertSessionHasNoErrors()
->assertRedirect(route('admin.posts.show', 1));
$this->assertDatabaseHas('posts', [
'title' => 'Tựa đề bài viết mới',
'slug' => 'tua-de-bai-viet-moi-test',
'status' => 'draft',
]);
}
/**
* Test 4: Cập nhật bài viết (Update)
*/
public function test_can_update_post(): void
{
// Tạo post ban đầu có user_id thuộc về $this->user
$post = Post::factory()->create([
'user_id' => $this->user->id,
'title' => 'Tựa đề cũ',
'slug' => 'tua-de-cu-test',
'content' => 'Nội dung cũ',
'status' => 'draft',
]);
$payload = [
'title' => 'Tựa đề đã cập nhật',
'slug' => 'tua-de-cu-test', // Giữ nguyên slug cũ để pass rule unique:posts,slug,{id}
'content' => 'Nội dung đã được chỉnh sửa mới',
'status' => 'published',
'user_id' => $this->user->id,
];
$response = $this->put(route('admin.posts.update', $post), $payload);
//$response->assertSessionHasNoErrors();
$response->assertSessionHasNoErrors()
->assertRedirect(route('admin.posts.show', 1));
$this->assertDatabaseHas('posts', [
'id' => $post->id,
'title' => 'Tựa đề đã cập nhật',
'status' => 'published',
]);
}
/**
* Test 5: Xóa mềm bài viết (Destroy)
*/
public function test_can_delete_post(): void
{
$post = Post::factory()->create([
'user_id' => $this->user->id,
'status' => 'published',
]);
$response = $this->delete(route('admin.posts.destroy', $post));
//$response->assertSessionHasNoErrors();
$response->assertSessionHasNoErrors()
->assertRedirect(route('admin.posts.index'));
$this->assertSoftDeleted('posts', [
'id' => $post->id,
]);
}
/**
* Test 6: Khôi phục bài viết đã xóa mềm (Restore - Route PATCH)
*/
public function test_can_restore_post(): void
{
$post = Post::factory()->create([
'status' => 'published',
]);
$post->delete();
$this->assertSoftDeleted('posts', [
'id' => $post->id,
]);
// Dùng URL trực tiếp /admin/posts/{id}/restore với PATCH để tránh bị route model binding chặn record đã soft delete
$response = $this->patch(route('admin.posts.restore', $post->id));
$this->assertNotSoftDeleted('posts', [
'id' => $post->id,
]);
}
}
//END classMột test đơn giản:
<?php
namespace Tests\Feature;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BlogTest extends TestCase
{
use RefreshDatabase;
public function test_blog_page_is_accessible(): void
{
$response = $this->get('/blog');
$response->assertOk();
}
public function test_post_can_be_viewed(): void
{
$post = Post::factory()->create();
$response = $this->get(
'/blog/' . $post->slug
);
$response->assertOk();
}
}
Chúng ta có thể tiếp tục mở rộng thành:
BlogTest
PostTest
CategoryTest
UserTest
ApiTest
AuthenticationTest
Với API Resource, đây là một test rất đáng làm:
$response->assertJsonStructure([
'data' => [
'*' => [
'id',
'title',
'slug',
],
],
]);
Nếu sau này developer vô tình xóa slug khỏi Resource:
Test FAIL
Nhờ vậy chúng ta phát hiện thay đổi API trước khi frontend hoặc mobile app gặp lỗi.
Một cách hiểu tốt hơn:
Test là một lớp bảo vệ cho source code.
Ví dụ:
Hôm nay
↓
Code chạy đúng
↓
Viết Test
↓
3 tháng sau
↓
Sửa Controller
↓
Chạy Test
↓
PASS
Nếu:
FAIL
chúng ta biết thay đổi mới có thể đã ảnh hưởng chức năng cũ.
Đây gọi là Regression Testing.
Ví dụ:
Version 1
↓
Login hoạt động
↓
Version 2
↓
Thêm API
↓
Version 3
↓
Sửa User
↓
Login bị lỗi
Nếu có test:
php artisan test
chúng ta có thể phát hiện lỗi nhanh hơn.
Khi chạy test, Laravel sử dụng environment:
testing
Các thiết lập testing có thể được cấu hình trong phpunit.xml hoặc .env.testing. Laravel cũng cấu hình một số driver phù hợp cho testing, chẳng hạn session và cache mặc định không ghi dữ liệu vào hệ thống thật.
Có thể tạo:
.env.testing
Ví dụ:
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
Tùy cấu hình project, bạn cũng có thể sử dụng một database testing riêng.
Điểm quan trọng:
Không chạy test trên database production.
Laravel project có:
phpunit.xml
File này chứa cấu hình dành cho PHPUnit.
Ví dụ có thể cấu hình:
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_STORE" value="array"/>
</php>
Không nên tùy tiện sửa cấu hình nếu chưa hiểu tác động của nó.
Ghi chú: chỉnh sửa trong đây phải clear config.
Không cần chạy toàn bộ test suite.
Có thể chỉ chạy:
php artisan test --filter=BlogTest
Hoặc:
vendor/bin/phpunit --filter BlogTest
Điều này rất hữu ích khi đang phát triển một module.
Khi xây dựng một feature:
Requirement
↓
Code
↓
Test
↓
Run Test
↓
PASS?
┌──┴──┐
YES NO
│ │
▼ ▼
Done Debug
│
▼
Fix
│
└──────► Test lại
Đây là vòng lặp rất quan trọng trong phát triển phần mềm.
Với project cuối khóa, không cần test mọi dòng code.
Có thể ưu tiên các chức năng quan trọng:
Authentication
↓
Authorization
↓
Categories
↓
Posts
↓
Upload
↓
Soft Delete
↓
REST API
↓
Sanctum
Ví dụ:
tests/
│
├── Feature/
│ ├── AuthenticationTest.php
│ ├── CategoryTest.php
│ ├── PostTest.php
│ ├── UserTest.php
│ └── ApiTest.php
│
└── Unit/
└── ExampleTest.php
Không cần biến project Blog CMS thành một hệ thống test khổng lồ.
Một người mới rất dễ nghĩ:
"Phải test 100% code."
Không nhất thiết.
100% coverage không đồng nghĩa:
100% không có bug
Điều quan trọng hơn là test những behavior quan trọng.
Ví dụ Blog CMS:
User không đăng nhập
→ không vào Admin
User thường
→ không quản lý User
Admin
→ quản lý User
Post draft
→ không xuất hiện public
Post published
→ xuất hiện Blog
API private
→ yêu cầu token
Upload sai file
→ validation fail
Đây là những behavior có giá trị cao để kiểm thử.
Khi đã có test, sau này có thể đưa vào CI/CD:
Git Push
│
▼
CI Server
│
├── composer install
├── Run Tests
├── Build Assets
└── Deploy
Nếu:
Tests PASS
→ có thể tiếp tục deployment.
Nếu:
Tests FAIL
→ dừng deployment.
Đây là bước phát triển tiếp theo sau khi đã nắm testing cơ bản.
Một số lệnh và assertion cần nhớ:
php artisan test
php artisan test --filter=BlogTest
$response->assertOk();
$response->assertStatus(200);
$response->assertRedirect('/login');
$response->assertSee('Blog');
$response->assertSessionHasErrors([
'title',
]);
$response->assertJson([
'data' => [],
]);
$response->assertJsonStructure([
'data',
]);
$response->assertUnauthorized();
$response->assertForbidden();
Tạo:
tests/Feature/BlogTest.php
Test:
GET /blog
Yêu cầu:
HTTP 200
Test Post:
GET /blog/{slug}
Yêu cầu:
HTTP 200
với một Post hợp lệ.
Test validation khi tạo Post.
Thiếu:
title
Kết quả:
validation error
Test API:
GET /api/posts
Yêu cầu:
HTTP 200
JSON
data
Test API private:
GET /api/user
Không có token:
401
Có token:
200
Test Upload Image.
Kiểm tra:
image hợp lệ
→ upload thành công
file không hợp lệ
→ validation fail
Không nhất thiết phải chờ project hoàn thành.
Có thể viết test ngay khi hoàn thành một feature:
Category
↓
Code
↓
Test
Post
↓
Code
↓
Test
API
↓
Code
↓
Test
Cách này giúp lỗi được phát hiện sớm.
Một workflow đơn giản:
1. Nhận requirement
↓
2. Thiết kế
↓
3. Code
↓
4. Viết Test
↓
5. Run Test
↓
6. Fix Bug
↓
7. Code Review
↓
8. Deploy
Ở project lớn hơn:
Developer
↓
Git
↓
CI
↓
Tests
↓
Build
↓
Deploy
Sau Bonus này, người học cần hiểu được:
PHPUnit
│
├── Unit Test
│
├── Feature Test
│
├── HTTP Test
│
├── Database Test
│
├── API Test
│
├── Authentication Test
│
├── Validation Test
│
├── Upload Test
│
└── Event / Queue / Mail Test
Và những assertion cơ bản:
assertOk()
assertStatus()
assertRedirect()
assertSee()
assertSessionHasErrors()
assertJson()
assertJsonStructure()
assertUnauthorized()
assertForbidden()Testing không phải là việc viết thật nhiều code test.
Điều quan trọng là xác định:
Ứng dụng phải làm gì?
Sau đó biến yêu cầu đó thành test.
Ví dụ:
Requirement:
"User chưa đăng nhập không được vào Admin."
↓
Test:
GET /admin/users
↓
Expected:
401 / Redirect Login
Hoặc:
Requirement:
"API Posts phải trả JSON."
↓
Test:
GET /api/posts
↓
Expected:
200
JSON
data
Hoặc:
Requirement:
"User thường không được quản lý User."
↓
Test:
User → GET /admin/users
↓
Expected:
403
Đó chính là tư duy testing.
x1
Cập nhật: 2026-10-07T20:22:08.528+07:00
Ở các bài trước, chúng ta đã lần lượt xây dựng và triển khai Blog CMS:
Bài 45 → Shared Hosting
Bài 46 → VPS
Bài 47 → Nginx
Bài 48 → Queue Worker & Scheduler
Đến đây ứng dụng đã có thể chạy trên production.
Nhưng:
Chạy được chưa có nghĩa là đã được cấu hình tốt cho production.
Một Laravel application thực tế cần quan tâm thêm:
Performance
Cache
Environment
Debug
Security
Database
Queue
Storage
Logs
Deployment
Configuration
Laravel 13 cung cấp sẵn nhiều công cụ để tối ưu những vấn đề này. Trong tài liệu deployment chính thức, Laravel đặc biệt khuyến nghị cache configuration, events, routes và views khi triển khai production.
Trong quá trình học, chúng ta thường chạy:
php artisan serve
với:
APP_ENV=local
APP_DEBUG=true
Điều này phù hợp cho development.
Production nên sử dụng:
APP_ENV=production
APP_DEBUG=false
Mô hình:
Development
APP_ENV=local
APP_DEBUG=true
│
▼
Debug nhiều
Hot reload
Test
Development
Production:
Production
APP_ENV=production
APP_DEBUG=false
│
▼
Security
Performance
Caching
Monitoring
.envLaravel sử dụng .env để chứa các giá trị phụ thuộc môi trường.
Ví dụ:
APP_NAME="Blog CMS"
APP_ENV=production
APP_KEY=base64:...
APP_DEBUG=false
APP_URL=https://example.com
Database:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=blog_user
DB_PASSWORD=********
Mail:
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_ENCRYPTION=tls
Queue:
QUEUE_CONNECTION=database
Cache:
CACHE_STORE=database
Các giá trị này có thể thay đổi giữa:
Local
Staging
Production
Laravel cũng khuyến cáo không commit .env vào source control vì file này có thể chứa credentials và thông tin nhạy cảm.
.env không phải file để đưa lên GitKhông nên:
git add .env
Thay vào đó:
.env
nên được ignore.
Thông thường project có:
.env
.env.example
Trong đó:
.envChứa cấu hình thực tế:
DB_PASSWORD=secret-password
.env.exampleChỉ chứa cấu trúc mẫu:
DB_PASSWORD=
Nhờ vậy người khác có thể biết application cần những biến môi trường nào mà không biết password production.
Ví dụ:
APP_ENV=production
Laravel có thể kiểm tra environment:
use Illuminate\Support\Facades\App;
if (App::environment('production')) {
// Production
}
Hoặc:
if (App::environment(['local', 'staging'])) {
// Local hoặc staging
}
Trong production Blog CMS:
APP_ENV=production
là lựa chọn phù hợp.
Đây là một trong những biến quan trọng nhất:
APP_DEBUG=false
Development:
APP_DEBUG=true
Production:
APP_DEBUG=false
Laravel documentation cảnh báo APP_DEBUG=true trên production có thể làm lộ thông tin nhạy cảm cho người dùng.
Giả sử code có lỗi:
$post->category->name;
nhưng:
$post->category
là null.
Development có thể hiển thị rất nhiều thông tin:
Exception
File
Line
Stack Trace
Environment
Configuration
SQL
Request
Điều này cực kỳ hữu ích khi debug.
Nhưng production:
APP_DEBUG=true
có thể biến thông tin debug thành dữ liệu mà attacker có thể khai thác.
Do đó:
APP_DEBUG=false
Production nên hiển thị một thông báo thân thiện:
Something went wrong.
Please try again later.
thay vì:
SQLSTATE...
/var/www/blog/vendor/...
DB_PASSWORD...
Stack trace...
Nguyên tắc:
Development
↓
Developer cần thông tin
Production
↓
User cần thông báo an toàn
Laravel application cần:
APP_KEY=base64:...
Không được tùy tiện thay đổi APP_KEY trên production đang chạy.
Nếu thay đổi key, những dữ liệu được mã hóa bằng key cũ có thể không còn giải mã được.
Ví dụ:
php artisan key:generate
Chỉ nên thực hiện khi tạo application hoặc khi bạn hiểu rõ tác động.
Không nên mỗi lần deploy lại chạy:
php artisan key:generate
Production nên đặt URL thật:
APP_URL=https://example.com
Không nên để:
APP_URL=http://127.0.0.1:8000
nếu application đang chạy trên Internet.
APP_URL có thể được sử dụng trong nhiều tình huống tạo URL, command và các thành phần khác của application.
Cache là kỹ thuật lưu lại dữ liệu hoặc kết quả đã được xử lý để lần sau lấy nhanh hơn.
Ví dụ:
Không Cache
Request
↓
Database
↓
Query
↓
Process
↓
Response
Có Cache:
Request
↓
Cache
│
├── Có → Response nhanh
│
└── Không
↓
Database
↓
Cache
↓
Response
Laravel cung cấp API thống nhất cho nhiều cache backend.
Ví dụ:
use Illuminate\Support\Facades\Cache;
Lưu:
Cache::put('site_name', 'Blog CMS', 3600);
Lấy:
$name = Cache::get('site_name');
Hoặc:
$name = Cache::get('site_name', 'Default');
Xóa:
Cache::forget('site_name');
Cache::remember()Đây là một pattern rất hữu ích.
Ví dụ:
$categories = Cache::remember(
'blog.categories',
3600,
function () {
return Category::active()
->withCount('posts')
->orderBy('name')
->get();
}
);
Luồng:
Request
│
▼
Cache?
│
├── Có
│ ↓
│ Return
│
└── Không
↓
Database
↓
Cache
↓
Return
Thay vì query database liên tục, dữ liệu có thể được sử dụng từ cache trong thời gian đã cấu hình.
Trong Blog CMS, chúng ta có:
Categories
Tags
Settings
Statistics
Popular Posts
Homepage Data
Một số dữ liệu không thay đổi liên tục có thể phù hợp với cache.
Ví dụ:
$categories = Cache::remember(
'categories.active',
3600,
fn () => Category::active()
->withCount('posts')
->orderBy('name')
->get()
);
Cache không phải:
Càng nhiều càng tốt.
Ví dụ:
User profile
Admin permissions
Realtime stock price
Current order status
có thể cần dữ liệu mới.
Nếu cache quá lâu:
Database
↓
Data mới
Cache
↓
Data cũ
Người dùng có thể nhìn thấy dữ liệu stale.
Do đó cần cân nhắc:
Data thay đổi thường xuyên
→ Cache ngắn hoặc không cache
Data ít thay đổi
→ Cache lâu hơn
Trong development hoặc khi troubleshooting:
php artisan optimize:clear
Laravel 13 cung cấp optimize:clear để xóa các cache được tạo bởi optimize và các key trong default cache store.
Có thể sử dụng riêng:
php artisan config:clear
hoặc:
php artisan route:clear
hoặc:
php artisan view:clear
Tùy trường hợp.
Laravel 13 có command:
php artisan optimize
Đây là command rất quan trọng khi deploy production.
Laravel sẽ cache các thành phần quan trọng như:
Configuration
Events
Routes
Views
Laravel documentation khuyến nghị chạy php artisan optimize trong deployment process.
config:cacheChạy:
php artisan config:cache
Laravel gom configuration vào cache.
Điều này giúp giảm việc phải đọc nhiều file configuration khi application boot.
env()Sau khi:
php artisan config:cache
không nên viết:
$name = env('APP_NAME');
trực tiếp trong application code.
Không nên:
class BlogController
{
public function index()
{
$debug = env('APP_DEBUG');
}
}
Thay vào đó:
$debug = config('app.debug');
Tư duy đúng:
.env
↓
config/*.php
↓
config(...)
Ví dụ trong:
config/app.php
có:
'debug' => (bool) env('APP_DEBUG', false),
Application sử dụng:
config('app.debug');
Laravel documentation lưu ý rằng sau khi config:cache, .env không còn được load trong request và env() bên ngoài configuration files có thể trả về null.
Laravel có:
php artisan route:cache
Command này tạo route cache.
Đặc biệt có lợi với application có nhiều routes.
Blog CMS của chúng ta có thể có:
Web Routes
Admin Routes
Blog Routes
API Routes
Auth Routes
Khi số lượng route tăng lên, route caching giúp giảm thời gian đăng ký routes khi application boot.
Chạy:
php artisan view:cache
Laravel sẽ precompile Blade views.
Ví dụ:
resources/views/
↓
Blade
↓
Compiled Views
Nhờ vậy production không phải compile từng Blade view lần đầu khi request.
Laravel khuyến nghị cache views trong deployment process.
Laravel cũng hỗ trợ:
php artisan event:cache
Command này cache event → listener mappings.
Ví dụ Blog CMS:
Registered
↓
SendWelcomeEmail
PostPublished
↓
SendNotification
PostUpdated
↓
ClearPostCache
Khi application lớn, caching event discovery có thể giúp quá trình boot hiệu quả hơn.
Có thể chạy:
php artisan optimize
Thay vì phải chạy riêng:
php artisan config:cache
php artisan event:cache
php artisan route:cache
php artisan view:cache
Laravel 13 cung cấp optimize như command tổng hợp cho deployment.
Khi deploy production:
composer install \
--no-dev \
--optimize-autoloader
Trong một dòng:
composer install --no-dev --optimize-autoloader
Ý nghĩa:
--no-dev
Không cài các package development không cần thiết.
Ví dụ:
PHPUnit
Debugbar
Development tools
--optimize-autoloader giúp Composer tối ưu class autoloading.
npm run dev trên productionDevelopment:
npm run dev
Production:
npm run build
Ví dụ:
npm install
npm run build
Sau đó:
public/build
chứa production assets.
Nếu Blog CMS sử dụng Vite:
resources/css
resources/js
thì production nên build:
npm run build
Thay vì để development server chạy trên production.
Mô hình:
Development
↓
npm run dev
Production
↓
npm run build
Một trong những bottleneck lớn nhất của web application thường là database.
Ví dụ:
Post::all();
có thể lấy quá nhiều dữ liệu.
Không nên:
$posts = Post::all();
nếu database có:
1,000,000 posts
Thay vào đó:
$posts = Post::paginate(10);
Blog CMS của chúng ta đã sử dụng:
->paginate(10)
Ví dụ:
$posts = Post::published()
->latest('published_at')
->paginate(10);
Thay vì lấy toàn bộ:
Post::all();
Database chỉ phải xử lý lượng dữ liệu phù hợp với page hiện tại.
Một lỗi rất phổ biến:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
echo $post->category->name;
}
Có thể tạo ra vấn đề N+1 Query.
Thay vào đó:
$posts = Post::with([
'user',
'category'
])->get();
Laravel sẽ eager load relationship.
Controller của chúng ta đã sử dụng:
Post::published()
->with(['user', 'category'])
->latest('published_at')
->paginate(10);
Đây là cách tốt hơn:
Posts
│
├── User
└── Category
thay vì để từng Post tự query User và Category.
Không phải lúc nào cũng cần:
Post::all();
với toàn bộ column.
Ví dụ:
Post::query()
->select([
'id',
'title',
'slug',
'excerpt'
])
->latest()
->paginate(10);
Nếu page chỉ hiển thị:
Title
Excerpt
thì không nhất thiết phải lấy toàn bộ content lớn.
Các column thường được:
WHERE
ORDER BY
JOIN
có thể cần index phù hợp.
Ví dụ:
posts.slug
posts.user_id
posts.category_id
posts.status
posts.published_at
Migration:
$table->index('status');
$table->index('published_at');
Foreign key thường đã có index phù hợp tùy schema và cách khai báo, nhưng cần hiểu execution plan và workload thực tế thay vì index mọi column.
Sai lầm:
Index tất cả
Index giúp query:
Nhanh hơn
nhưng cũng có cost:
INSERT
UPDATE
DELETE
phải duy trì index.
Do đó:
Query thường xuyên
↓
Phân tích
↓
Index phù hợp
Không phải:
Column nào cũng index
Không nên cache mọi query.
Ví dụ categories:
$categories = Cache::remember(
'categories.active',
3600,
fn () => Category::active()
->withCount('posts')
->orderBy('name')
->get()
);
Khi category thay đổi:
Cache::forget('categories.active');
Như vậy cache có thể được invalidated đúng thời điểm.
Một trong những vấn đề khó của cache là:
Cache cũ
Ví dụ:
10:00
Category = Laravel
cache:
Laravel
11:00 admin đổi:
Laravel → Laravel 13
nhưng cache vẫn:
Laravel
Nếu không clear/invalidate:
Database = Laravel 13
Cache = Laravel
Vì vậy khi update dữ liệu quan trọng:
Cache::forget('categories.active');
hoặc sử dụng strategy phù hợp.
Production cần kiểm tra:
php artisan storage:link
Laravel sử dụng:
storage/app/public
cho public files và symbolic link:
public/storage
để browser có thể truy cập.
Blog CMS:
Post Image
↓
storage/app/public/posts
↓
public/storage
↓
Browser
Laravel cần quyền ghi cho:
storage/
bootstrap/cache/
Ví dụ trên Ubuntu + Nginx/PHP-FPM:
sudo chown -R www-data:www-data /var/www/blog/storage
sudo chown -R www-data:www-data /var/www/blog/bootstrap/cache
Tùy server setup, user chạy PHP-FPM có thể khác www-data.
Không nên dùng:
chmod -R 777
chỉ để “cho chạy được”.
chmod 777 nguy hiểm?chmod -R 777 /var/www/blog
nghĩa là:
Owner → read/write/execute
Group → read/write/execute
Others → read/write/execute
Đây là quyền quá rộng.
Thay vào đó:
Owner
Group
Web Server
nên được cấu hình đúng.
Nguyên tắc:
Cấp quyền tối thiểu cần thiết.
.envKhông bao giờ public:
.env
Nginx phải trỏ:
root /var/www/blog/public;
chứ không:
root /var/www/blog;
Laravel deployment documentation nhấn mạnh rằng web server phải hướng request tới public/index.php, không phải project root, để tránh expose sensitive files.
Production:
APP_DEBUG=false
Không:
APP_DEBUG=true
Đây là một trong những cấu hình bảo mật cơ bản nhất của Laravel production.
Production:
APP_ENV=production
Giúp application biết mình đang chạy trong environment nào.
Các thông tin như:
APP_KEY
DB_PASSWORD
MAIL_PASSWORD
AWS_SECRET
API_KEY
không nên hard-code vào source:
$password = '123456';
Không nên:
Git
↓
Source
↓
Secret
Mà:
Environment
↓
.env
↓
config
↓
Application
Laravel không nên lưu password dạng plain text.
Không:
password = 123456
Laravel sử dụng hashing.
Ví dụ:
use Illuminate\Support\Facades\Hash;
$hash = Hash::make($password);
Kiểm tra:
Hash::check($password, $hash);
Không tin dữ liệu từ browser.
Ví dụ:
$request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
]);
Client-side validation:
JavaScript
chỉ giúp UX.
Server-side validation:
Laravel
mới là lớp bảo vệ thực sự.
Model:
protected $fillable = [
'name',
'slug',
'description',
'is_active',
];
Không nên cho phép user tùy tiện gửi:
role=admin
nếu role không thuộc dữ liệu họ được phép thay đổi.
Ví dụ:
User::create($request->validated());
cần kết hợp:
Validation
+
Fillable
+
Authorization
Authentication:
Bạn là ai?
Authorization:
Bạn được phép làm gì?
Ví dụ:
User
└── Xem bài viết
Admin
├── Tạo category
├── Sửa category
├── Xóa category
└── Quản lý user
Không nên chỉ dựa vào:
ẩn button
mà phải bảo vệ ở server bằng:
Middleware
Policy
Gate
Authorization
Các form web thay đổi dữ liệu cần CSRF protection.
Ví dụ Blade:
<form method="POST" action="{{ route('posts.store') }}">
@csrf
...
</form>
Laravel có cơ chế CSRF protection cho web requests.
Không nên bỏ:
@csrf
chỉ vì form vẫn submit được trong một số trường hợp.
Không nên nối chuỗi SQL trực tiếp:
DB::select(
"SELECT * FROM posts WHERE title = '$title'"
);
Thay vào đó sử dụng Query Builder/Eloquent:
Post::where('title', $title)->get();
Laravel sẽ sử dụng parameter binding phù hợp.
Nếu nội dung user nhập:
<script>...</script>
không nên tùy tiện render raw HTML.
Blade mặc định:
{{ $post->title }}
sẽ escape output.
Cẩn thận với:
{!! $post->content !!}
Nếu content cho phép HTML, cần có chiến lược sanitization phù hợp.
Blog CMS của chúng ta có upload image.
Không nên chỉ kiểm tra:
filename.jpg
Mà phải validation:
'image' => [
'nullable',
'image',
'max:2048',
]
Có thể giới hạn MIME/types theo yêu cầu application.
Ngoài server validation, frontend có thể báo:
File > 2MB
để cải thiện UX.
Nhưng validation Laravel vẫn bắt buộc.
Không nên lưu file upload tùy tiện vào:
project root
Nên sử dụng Laravel filesystem:
$image->store('posts', 'public');
Sau đó:
php artisan storage:link
Laravel filesystem cung cấp abstraction cho local, public và các storage backend khác.
Production nên sử dụng:
HTTPS
Không nên:
HTTP
để đăng nhập.
Mô hình:
Browser
│
HTTPS
▼
Nginx
▼
Laravel
HTTPS bảo vệ dữ liệu trên đường truyền.
Có thể cấu hình một số security headers tại Nginx.
Ví dụ:
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
Laravel 13 deployment configuration mẫu cũng sử dụng các header này.
Không nên copy một danh sách security headers một cách máy móc.
Mỗi header cần hiểu rõ tác dụng và khả năng ảnh hưởng tới application.
Các endpoint nhạy cảm như:
Login
Register
Password Reset
API
nên có rate limiting phù hợp.
Ví dụ:
User
↓
Login attempts
↓
Rate Limit
↓
Too many requests
↓
Block / Throttle
Điều này giúp giảm abuse và brute-force attempts.
Production không nên bật debug để “xem lỗi”.
Hãy sử dụng logs.
Ví dụ:
Log::error('Payment failed', [
'order_id' => $order->id,
]);
Log thường nằm trong:
storage/logs/
Có thể kiểm tra:
tail -f storage/logs/laravel.log
hoặc:
tail -f storage/logs/*.log
Laravel 13 có health route:
/up
Mặc định route này trả:
200
nếu application boot thành công.
Nếu application gặp lỗi boot:
500
Laravel cung cấp route này để uptime monitor, load balancer hoặc hệ thống orchestration sử dụng.
Ví dụ:
https://example.com/up
Có thể dùng:
Monitoring
│
▼
https://example.com/up
│
├── 200 → OK
│
└── 500 → Problem
Đây là một cách đơn giản để kiểm tra application còn hoạt động hay không.
Từ Bài 45 đến Bài 49, quy trình deployment của chúng ta có thể là:
Developer
│
▼
Git
│
▼
VPS
│
▼
composer install --no-dev --optimize-autoloader
│
▼
npm run build
│
▼
php artisan migrate --force
│
▼
php artisan optimize
│
▼
php artisan reload
│
▼
Nginx
│
▼
Laravel 13
Laravel 13 deployment documentation cũng khuyến nghị tối ưu application và reload các long-running services sau deployment để chúng sử dụng code mới.
Có thể hình dung quy trình:
git pull origin main
composer install --no-dev --optimize-autoloader
npm install
npm run build
php artisan migrate --force
php artisan storage:link
php artisan optimize
php artisan reload
Tùy kiến trúc Queue/Worker, có thể cần đảm bảo process monitor như Supervisor khởi động lại worker sau khi reload.
Development:
php artisan migrate
Production:
php artisan migrate --force
Flag:
--force
cho phép migration chạy trong production mà không yêu cầu interactive confirmation.
Nhưng:
Trước khi migration production, phải hiểu migration sẽ thay đổi database như thế nào.
Đặc biệt với database lớn:
ALTER TABLE
DROP COLUMN
Index
Data migration
có thể ảnh hưởng production.
Một production application phải nghĩ đến:
Nếu database hỏng thì sao?
Không nên chỉ có:
Database
mà cần:
Database
│
▼
Backup
│
├── Daily
├── Weekly
└── Retention
Ví dụ MySQL có thể backup bằng:
mysqldump
Điều quan trọng không chỉ là:
Backup
mà còn:
Restore được hay không?
Một backup chưa từng thử restore chưa thể xem là chiến lược backup hoàn chỉnh.
Database không phải dữ liệu duy nhất.
Blog CMS còn có:
Post Images
User Avatars
Uploaded Files
Nếu các file này nằm trong:
storage/app/public
thì cũng cần có chiến lược backup.
Production backup:
Database
+
Uploaded Files
+
Environment/Secrets strategy
vendor và node_modules như dữ liệu chínhThông thường source repository có:
composer.json
composer.lock
package.json
package-lock.json
Có thể cài lại:
composer install
npm install
Do đó production backup nên tập trung vào:
Database
Uploaded Files
Application Source
Configuration/Secrets
và chiến lược khôi phục phù hợp.
Một Laravel production cơ bản:
[✓] PHP production configuration
[✓] OPcache
[✓] Composer optimized autoload
[✓] Config cache
[✓] Route cache
[✓] Event cache
[✓] View cache
[✓] Database indexes
[✓] Pagination
[✓] Eager loading
[✓] Appropriate cache
[✓] Queue
[✓] Scheduler
[✓] Nginx
[✓] PHP-FPM
Không phải tất cả đều phải tối ưu cực đại ngay từ đầu.
Nguyên tắc:
Đo → tìm bottleneck → tối ưu.
Ví dụ application chậm.
Không nên ngay lập tức:
Redis
Horizon
Octane
Multiple servers
Load Balancer
Microservices
trong khi vấn đề thực sự là:
N+1 Query
hoặc:
SELECT *
hoặc:
Không có index
Quy trình tốt hơn:
Problem
↓
Measure
↓
Identify bottleneck
↓
Optimize
↓
Measure again
Sau 49 bài, Blog CMS đã đi từ:
PHP
đến:
Laravel 13
và cuối cùng:
INTERNET
│
▼
HTTPS
│
▼
Nginx
│
┌───────────┴───────────┐
│ │
PHP-FPM Static Files
│
▼
Laravel 13
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
MySQL Cache Storage
│
│
▼
Database
Queue:
Laravel → Queue → Worker → Supervisor
Scheduler:
Cron → schedule:run → Laravel Scheduler
Đây không còn là một project PHP đơn giản.
Đây đã là một Laravel application có kiến trúc production cơ bản.
.env mẫuVí dụ:
APP_NAME="Blog CMS"
APP_ENV=production
APP_KEY=base64:YOUR_REAL_APP_KEY
APP_DEBUG=false
APP_URL=https://example.com
LOG_CHANNEL=stack
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=blog_user
DB_PASSWORD=YOUR_DATABASE_PASSWORD
QUEUE_CONNECTION=database
CACHE_STORE=database
Không copy nguyên mẫu này vào production.
Các giá trị:
APP_KEY
DB_PASSWORD
MAIL_PASSWORD
API_KEY
phải sử dụng secret thật của server.
Sau khi deploy code:
composer install --no-dev --optimize-autoloader
Build frontend:
npm run build
Migration:
php artisan migrate --force
Storage:
php artisan storage:link
Optimization:
php artisan optimize
Reload:
php artisan reload
Nếu sử dụng Supervisor cho queue worker, kiểm tra:
sudo supervisorctl status
Kiểm tra Nginx:
sudo nginx -t
Kiểm tra application:
https://example.com/up
optimize:clear?Nếu production có vấn đề liên quan đến cache configuration:
php artisan optimize:clear
Sau đó kiểm tra lại .env và configuration.
Sau khi sửa xong, có thể chạy:
php artisan optimize
Tư duy:
Clear
↓
Fix
↓
Optimize
Không nên chạy optimize:clear liên tục trên production mà không hiểu lý do.
APP_DEBUG=true
Production.
→ Có nguy cơ lộ thông tin.
Nginx:
root /var/www/blog;
→ Sai.
Đúng:
root /var/www/blog/public;
Không chạy:
php artisan optimize
→ Có thể bỏ qua các optimization cache production.
Deploy code mới nhưng Worker vẫn chạy code cũ.
→ Reload/restart long-running services.
Không có backup database.
→ Khi database hỏng, mất dữ liệu.
Upload file nhưng không:
php artisan storage:link
→ Image không truy cập được.
Dùng:
chmod -R 777
→ Cấp quyền quá rộng.
Query:
Model::all();
trên bảng cực lớn.
→ Có thể gây tốn RAM và chậm.
[ ] APP_DEBUG=false
[ ] APP_ENV=production
[ ] HTTPS
[ ] .env không commit Git
[ ] APP_KEY được bảo vệ
[ ] Database password được bảo vệ
[ ] Không expose project root
[ ] Nginx root → public/
[ ] Validation server-side
[ ] Authorization
[ ] CSRF
[ ] Password hashing
[ ] File upload validation
[ ] Rate limiting
[ ] Security headers phù hợp
[ ] Logs
[ ] Database backup
[ ] Uploaded files backup
[ ] Queue Worker monitoring
[ ] Composer optimized autoload
[ ] Config cache
[ ] Event cache
[ ] Route cache
[ ] View cache
[ ] Database indexes
[ ] Pagination
[ ] Eager loading
[ ] Query optimization
[ ] Cache dữ liệu phù hợp
[ ] Vite production build
[ ] Queue cho tác vụ nặng
[ ] Scheduler cho tác vụ định kỳ
Đổi:
APP_ENV=production
APP_DEBUG=false
Sau đó kiểm tra:
App::environment();
Chạy:
php artisan optimize
Sau đó kiểm tra website.
Chạy:
php artisan optimize:clear
Sau đó:
php artisan optimize
Quan sát sự khác nhau.
Chạy:
php artisan route:cache
Sau đó:
php artisan route:list
Chạy:
php artisan view:cache
Kiểm tra Blog CMS.
Viết:
Cache::remember(...)
để cache danh sách category.
Sau đó khi category thay đổi:
Cache::forget(...)
Mở:
/up
và kiểm tra HTTP status.
Kiểm tra production:
APP_DEBUG=false
và thử truy cập:
/.env
Kết quả phải là:
Không được phép truy cập
Sau bài này, chúng ta đã hoàn thành phần quan trọng nhất của deployment Laravel 13.
Các nguyên tắc cần nhớ:
ENV
↓
Configuration
↓
Cache
↓
Optimization
↓
Security
↓
Monitoring
Production không đơn giản chỉ là:
Upload source code
Mà là:
Application
+
Web Server
+
PHP-FPM
+
Database
+
Cache
+
Queue
+
Scheduler
+
Security
+
Monitoring
+
Backup
php artisan optimize
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan storage:link
php artisan migrate --force
php artisan reload
Laravel 13 documentation xác nhận reload được dùng để terminate các long-running services như queue workers, Reverb hoặc Octane sau deployment để process monitor có thể khởi động lại chúng với code mới.
Một developer Laravel không chỉ cần biết:
Controller
Model
Migration
Blade
Route
mà khi đưa application ra Internet còn phải hiểu:
Code
↓
Security
↓
Performance
↓
Deployment
↓
Monitoring
↓
Backup
Đó chính là sự khác biệt giữa:
"Làm được một Laravel project"
và:
"Biết triển khai một Laravel application"
Sau:
Bài 45 — Deploy Shared Hosting
Bài 46 — Deploy VPS
Bài 47 — Nginx
Bài 48 — Queue Worker & Scheduler
Bài 49 — Production Tips
chúng ta đã hoàn thành toàn bộ phần:
TRIỂN KHAI LARAVEL 13
Kiến trúc cuối cùng:
INTERNET
│
HTTPS
│
▼
NGINX
│
▼
PHP-FPM
│
▼
┌─────────────┐
│ Laravel 13 │
└──────┬──────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
MySQL Cache Storage
│
│
▼
Database
Laravel
│
▼
Queue
│
▼
Worker
│
▼
Supervisor
Cron
│
▼
Laravel Scheduler
Và chỉ còn một bài cuối cùng:
Chúng ta sẽ tổng hợp toàn bộ những gì đã học:
PHP
↓
Laravel 13
↓
MVC
↓
Database
↓
Eloquent
↓
Relationship
↓
Authentication
↓
Authorization
↓
CRUD
↓
Upload
↓
Search
↓
Pagination
↓
Soft Delete
↓
Events
↓
Queue
↓
Mail
↓
Notification
↓
Cache
↓
Storage
↓
Logging
↓
REST API
↓
Sanctum
↓
Nginx
↓
VPS
↓
Production
và ráp lại thành Blog CMS hoàn chỉnh.
x1
Cập nhật: 2026-09-27T15:53:12.479+07:00
Ở Bài 47, chúng ta đã cấu hình:
Internet
↓
Nginx
↓
PHP-FPM
↓
Laravel 13
Nhưng một ứng dụng thực tế thường không chỉ xử lý request ngay lập tức.
Ví dụ Blog CMS của chúng ta có thể cần:
gửi email;
gửi notification;
xử lý ảnh;
tạo báo cáo;
xóa dữ liệu cũ;
đồng bộ dữ liệu;
gửi email hàng loạt;
chạy tác vụ mỗi ngày;
dọn database;
kiểm tra bài viết scheduled.
Nếu bắt người dùng chờ tất cả những việc này hoàn thành trong HTTP request thì website sẽ chậm.
Laravel cung cấp hai cơ chế rất quan trọng:
Queue
Scheduler
Trong bài này chúng ta sẽ đưa cả hai lên VPS production.
Queue có thể hiểu đơn giản là:
Đưa một công việc vào hàng đợi để xử lý sau.
Ví dụ người dùng đăng ký tài khoản:
User Register
│
▼
Laravel
│
├── Lưu User
│
└── Đưa Email vào Queue
│
▼
Worker
│
▼
Gửi Email
Thay vì:
User
│
▼
Register
│
▼
Gửi Email
│
▼
Chờ SMTP
│
▼
Response
chúng ta có:
User
│
▼
Register
│
▼
Queue Job
│
▼
Response nhanh
Sau đó Worker xử lý email ở background.
Giả sử Blog CMS có chức năng gửi email cho 10.000 người dùng.
Nếu xử lý trực tiếp:
foreach ($users as $user) {
Mail::to($user)->send(...);
}
request có thể phải chờ rất lâu.
Không nên bắt browser chờ:
Browser
│
│ Request
▼
Laravel
│
├── Email 1
├── Email 2
├── Email 3
├── ...
└── Email 10000
│
▼
Response
Thay vào đó:
Browser
│
▼
Laravel
│
▼
Queue
│
└── 10000 jobs
│
▼
Worker
Browser nhận response sớm hơn.
Một Job đại diện cho một công việc.
Ví dụ:
SendWelcomeEmail
hoặc:
ProcessPostImage
hoặc:
GenerateReport
Tạo Job:
php artisan make:job SendWelcomeEmail
Laravel tạo:
app/Jobs/SendWelcomeEmail.php
Ví dụ:
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SendWelcomeEmail implements ShouldQueue
{
use Queueable;
public function __construct(
public User $user
) {
}
public function handle(): void
{
// Xử lý công việc
}
}
Điểm quan trọng:
implements ShouldQueue
cho Laravel biết Job này cần được xử lý thông qua Queue.
Sau khi tạo Job:
SendWelcomeEmail::dispatch($user);
Ví dụ:
use App\Jobs\SendWelcomeEmail;
SendWelcomeEmail::dispatch($user);
Lúc này ứng dụng không nhất thiết phải thực hiện toàn bộ công việc ngay trong request.
Job được đưa vào Queue để Worker xử lý.
Laravel hỗ trợ nhiều queue backend.
Một số lựa chọn phổ biến:
Database
Redis
Amazon SQS
Trong khóa học, chúng ta bắt đầu bằng:
Database Queue
vì dễ hiểu và phù hợp để học.
Database Queue lưu Job vào database.
Mô hình:
Laravel
│
▼
jobs table
│
▼
Queue Worker
│
▼
handle()
Laravel có thể tạo migration cho jobs table bằng:
php artisan make:queue-table
Sau đó:
php artisan migrate
Database sẽ có bảng:
jobs
Ngoài ra có thể có:
failed_jobs
để lưu những Job thất bại.
Trong .env:
QUEUE_CONNECTION=database
Sau khi thay đổi .env, trong production nên đảm bảo configuration cache được cập nhật.
Ví dụ:
php artisan config:clear
hoặc sau khi deploy:
php artisan optimize
Đây là phần cực kỳ quan trọng.
Queue chỉ chứa Job.
Ai xử lý Job?
Queue Worker
Chạy:
php artisan queue:work
Worker sẽ:
Queue
│
├── Job 1
├── Job 2
├── Job 3
└── Job 4
│
▼
Worker
│
▼
handle()
queue:work chạy liên tụcKhi chạy:
php artisan queue:work
Laravel Worker không chỉ xử lý một Job rồi kết thúc.
Nó tiếp tục chờ:
Job
↓
Process
↓
Job
↓
Process
↓
Job
↓
Process
↓
...
Đây là một long-running process.
Vì vậy production cần một process manager để đảm bảo Worker luôn được chạy.
Ví dụ:
php artisan queue:work
Bạn SSH vào VPS và chạy.
Sau đó:
SSH disconnect
Worker có thể kết thúc.
Đây không phải cách triển khai production tốt.
Chúng ta cần:
Supervisor
hoặc một process manager tương đương.
Supervisor là process manager.
Nó có nhiệm vụ:
Kiểm tra Worker
│
├── Worker đang chạy
│ ↓
│ OK
│
└── Worker chết
↓
Restart
Như vậy:
Laravel Queue
↓
queue:work
↓
Supervisor
↓
Luôn duy trì Worker
Trên VPS:
sudo apt update
Sau đó:
sudo apt install supervisor
Kiểm tra:
sudo systemctl status supervisor
Giả sử project:
/var/www/blog
Tạo:
sudo nano /etc/supervisor/conf.d/blog-worker.conf
Nội dung:
[program:blog-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/blog/artisan queue:work database --sleep=3 --tries=3 --timeout=90
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/blog/storage/logs/worker.log
stopwaitsecs=3600
Cấu hình này tạo:
Worker 1
Worker 2
numprocsnumprocs=2
nghĩa là chạy hai Worker.
Supervisor
│
├── Worker 1
│
└── Worker 2
Nếu có nhiều Job:
Job 1 ── Worker 1
Job 2 ── Worker 2
Job 3 ── Worker 1
Job 4 ── Worker 2
Nhờ đó có thể xử lý nhiều công việc đồng thời.
Không nên tăng numprocs tùy tiện.
VPS ít CPU/RAM mà chạy quá nhiều Worker có thể làm server quá tải.
--sleep--sleep=3
Worker sẽ chờ 3 giây trước khi kiểm tra Queue lại khi không có Job.
Ví dụ:
Không có Job
↓
Chờ 3 giây
↓
Kiểm tra
↓
Không có
↓
Chờ 3 giây
--tries--tries=3
Nếu Job thất bại:
Attempt 1
↓
Fail
↓
Attempt 2
↓
Fail
↓
Attempt 3
↓
Fail
↓
Failed Job
Không phải mọi Job đều nên retry vô hạn.
--timeout--timeout=90
Worker cho phép Job chạy tối đa khoảng thời gian cấu hình trước khi bị timeout.
Con số thực tế cần được chọn dựa trên loại Job.
Ví dụ:
Gửi email
có thể cần thời gian khác:
Xử lý video
hoặc:
Generate report
Không nên đặt timeout quá thấp đối với Job cần nhiều thời gian.
Sau khi tạo configuration:
sudo supervisorctl reread
Sau đó:
sudo supervisorctl update
Kiểm tra:
sudo supervisorctl status
Ví dụ:
blog-worker:blog-worker_00 RUNNING
blog-worker:blog-worker_01 RUNNING
Khi deploy code mới, có một vấn đề quan trọng.
Queue Worker là process chạy lâu dài.
Nó có thể tiếp tục sử dụng code cũ đã được load vào memory.
Laravel cung cấp:
php artisan queue:restart
để yêu cầu Worker restart một cách graceful sau khi hoàn thành Job hiện tại. Laravel documentation cũng lưu ý rằng queue workers là long-lived processes và cần được restart/reload sau deployment.
Với Laravel 13, có thêm command tổng quát:
php artisan reload
để terminate các reloadable long-running services sau deployment; process monitor như Supervisor có thể khởi động chúng lại.
Trong mô hình VPS + Supervisor của khóa học, có thể dùng:
php artisan queue:restart
sau deployment.
Một Job có thể thất bại.
Ví dụ:
SMTP Server
↓
Connection failed
hoặc:
API
↓
Timeout
Laravel có thể lưu Job thất bại trong:
failed_jobs
Xem:
php artisan queue:failed
Có thể retry:
php artisan queue:retry all
hoặc một Job cụ thể:
php artisan queue:retry 5
Trong đó:
5
là ID của failed Job.
Có thể xóa một Job:
php artisan queue:forget 5
Hoặc xóa toàn bộ failed jobs:
php artisan queue:flush
Cẩn thận với:
queue:flush
vì nó xóa danh sách failed jobs.
Blog CMS có thể có:
Post Published
│
▼
SendPostPublishedNotification
│
▼
Queue
│
▼
Worker
│
├── Email
└── Notification
Controller không cần xử lý tất cả công việc nặng trong request.
Laravel Mail có thể được queue.
Ví dụ Mailable:
<?php
namespace App\Mail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
class WelcomeMail extends Mailable implements ShouldQueue
{
public function build()
{
return $this->subject('Welcome')
->view('emails.welcome');
}
}
Khi Mailable implement:
ShouldQueue
email sẽ được đưa vào Queue thay vì bắt request phải chờ việc gửi email hoàn thành. Laravel 13 documentation hỗ trợ queued mailables thông qua ShouldQueue.
Ở Bài 34 — Event & Listener, chúng ta đã học Event và Listener.
Listener cũng có thể chạy Queue.
Ví dụ:
use Illuminate\Contracts\Queue\ShouldQueue;
class SendPostNotification implements ShouldQueue
{
public function handle(PostPublished $event): void
{
// Send notification
}
}
Mô hình:
Event
│
▼
Listener
│
├── ShouldQueue
│
▼
Queue
│
▼
Worker
Laravel 13 hỗ trợ queued listeners và các cơ chế retry/backoff cho listener.
Laravel 13 có thêm khả năng queue routing theo Job class.
Ví dụ:
use Illuminate\Support\Facades\Queue;
Queue::route(
ProcessPodcast::class,
connection: 'redis',
queue: 'podcasts'
);
Điều này cho phép định tuyến mặc định cho một Job cụ thể thay vì phải cấu hình riêng ở từng nơi. Đây là một trong những thay đổi mới của Laravel 13.
Đối với khóa học cơ bản, chúng ta chưa cần sử dụng tính năng này ngay.
Điều quan trọng trước tiên là hiểu:
Job
↓
Queue
↓
Worker
Queue xử lý:
Công việc đang chờ
Scheduler xử lý:
Công việc cần chạy theo thời gian
Ví dụ:
Mỗi phút
Mỗi 5 phút
Mỗi giờ
Mỗi ngày
Mỗi tuần
Ví dụ Blog CMS:
00:00 mỗi ngày
↓
Dọn dữ liệu cũ
hoặc:
Mỗi 5 phút
↓
Kiểm tra bài viết scheduled
Có Job
↓
Xử lý Job
Đến thời gian
↓
Chạy Task
Có thể kết hợp:
Scheduler
│
▼
Dispatch Job
│
▼
Queue
│
▼
Worker
Đây là mô hình rất mạnh.
Trong Laravel hiện đại, scheduler có thể được định nghĩa trong:
routes/console.php
Ví dụ:
use Illuminate\Support\Facades\Schedule;
Schedule::call(function () {
// Task
})->daily();
Laravel scheduler cho phép định nghĩa lịch chạy ngay trong source code thay vì phải tạo từng cron entry riêng cho từng task.
Giả sử chúng ta có command:
php artisan make:command CleanOldPosts
Sau đó có thể schedule:
use Illuminate\Support\Facades\Schedule;
Schedule::command('posts:clean')
->daily();
Mỗi ngày Laravel sẽ chạy:
posts:clean
Có thể schedule Job:
use App\Jobs\GenerateDailyReport;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new GenerateDailyReport)
->daily();
Mô hình:
Scheduler
│
│ mỗi ngày
▼
GenerateDailyReport
│
▼
Queue
│
▼
Worker
Đây là cách kết hợp Scheduler và Queue rất phổ biến.
Ví dụ:
Schedule::command('posts:clean')
->daily();
Mỗi giờ:
Schedule::command('posts:sync')
->hourly();
Mỗi 5 phút:
Schedule::command('posts:sync')
->everyFiveMinutes();
Mỗi 10 phút:
Schedule::command('posts:sync')
->everyTenMinutes();
Mỗi ngày lúc 02:00:
Schedule::command('posts:clean')
->dailyAt('02:00');
Đây là điểm rất quan trọng.
Laravel Scheduler không tự nhiên chạy chỉ vì chúng ta viết:
Schedule::command(...);
Trên production, server cần kích hoạt scheduler.
Một cách phổ biến là Cron chạy:
php artisan schedule:run
mỗi phút.
Cron:
Every minute
│
▼
schedule:run
│
▼
Laravel kiểm tra Schedule
│
├── Task đến giờ?
│ │
│ └── Có → chạy
│
└── Chưa đến giờ → bỏ qua
Laravel documentation mô tả mô hình này: chỉ cần một cron entry chạy schedule:run mỗi phút; Laravel tự kiểm tra các task đã được định nghĩa.
Trên VPS:
crontab -e
Thêm:
* * * * * cd /var/www/blog && php artisan schedule:run >> /dev/null 2>&1
Ý nghĩa:
* * * * *
= mỗi phút.
Sau đó:
cd /var/www/blog
đi vào Laravel project.
Rồi:
php artisan schedule:run
Giả sử có:
Schedule::command('posts:clean')
->dailyAt('02:00');
Cron vẫn chạy:
01:58 → schedule:run
01:59 → schedule:run
02:00 → schedule:run
02:01 → schedule:run
Nhưng chỉ tại:
02:00
Laravel thực hiện:
posts:clean
Các phút còn lại:
Không có task cần chạy
Trong môi trường development có thể chạy:
php artisan schedule:work
Scheduler sẽ chạy foreground và liên tục kiểm tra schedule. Đây là cách thuận tiện để test scheduler local thay vì cấu hình Cron trên máy development.
Blog CMS của chúng ta có thể có:
Scheduler
│
├── Every minute
│ └── Publish scheduled posts
│
├── Every hour
│ └── Update statistics
│
├── Daily
│ └── Cleanup old data
│
└── Weekly
└── Generate report
Ví dụ:
use Illuminate\Support\Facades\Schedule;
Schedule::command('posts:publish-scheduled')
->everyMinute();
Schedule::command('stats:update')
->hourly();
Schedule::command('cleanup:old-data')
->dailyAt('02:00');
Một task có thể mất nhiều thời gian.
Ví dụ:
Task bắt đầu
│
├── chạy 10 phút
│
└── Task tiếp theo lại được gọi
Có thể dẫn tới:
Task #1 đang chạy
Task #2 chạy cùng lúc
Nếu không muốn điều này, có thể dùng:
Schedule::command('reports:generate')
->daily()
->withoutOverlapping();
Khi đó Laravel cố gắng tránh việc task mới bắt đầu trong khi instance trước vẫn đang chạy.
Nếu hệ thống có nhiều server:
Server 1
Server 2
Server 3
Scheduler có thể cần tránh việc cùng một task chạy trên cả ba server.
Trong các trường hợp phù hợp, Laravel cung cấp cơ chế:
->onOneServer()
Ví dụ:
Schedule::command('reports:generate')
->daily()
->onOneServer();
Cơ chế này cần cache backend phù hợp để các server phối hợp với nhau.
Đây là kiến trúc rất đáng nhớ:
Scheduler
│
│ mỗi ngày
▼
GenerateReport
│
▼
Queue
│
▼
Worker
│
▼
Report generated
Scheduler quyết định:
Khi nào?
Queue quyết định:
Xử lý công việc ở background như thế nào?
Một ví dụ thực tế:
BLOG CMS
│
┌────────────┴────────────┐
│ │
Queue Scheduler
│ │
▼ ▼
Send Mail Scheduled Posts
Notification Daily Cleanup
Process Image Daily Report
│ │
▼ ▼
Worker Artisan/Job
Đây chính là kiến trúc production mà chúng ta đang xây dựng.
Khi deploy version mới:
git pull
Sau đó:
composer install --no-dev --optimize-autoloader
Nếu có frontend:
npm install
npm run build
Migration:
php artisan migrate --force
Cache:
php artisan optimize
Sau đó cần xử lý Worker.
Có thể:
php artisan queue:restart
hoặc sử dụng:
php artisan reload
để reload các long-running services mà Laravel quản lý, tùy kiến trúc deployment.
Với VPS sử dụng Supervisor:
Deploy
↓
queue:restart
↓
Worker graceful exit
↓
Supervisor phát hiện process kết thúc
↓
Worker mới
↓
Code mới
Giả sử:
10:00
Deploy Version 1
Worker load:
Version 1
Sau đó:
10:30
Deploy Version 2
Nếu Worker vẫn đang chạy:
Worker
↓
Version 1
trong khi website:
Nginx
↓
Laravel Version 2
Có thể xảy ra:
Website = Version 2
Worker = Version 1
Vì vậy deployment cần reload/restart long-running processes.
Kiến trúc hoàn chỉnh:
VPS
│
┌─────────┴─────────┐
│ │
Nginx Supervisor
│ │
▼ ▼
PHP-FPM Queue Worker
│ │
▼ ▼
Laravel 13 Queue Backend
│
▼
MySQL
Scheduler:
Cron
│
▼
schedule:run
│
▼
Laravel Scheduler
Trên VPS:
sudo supervisorctl status
Ví dụ:
blog-worker:blog-worker_00 RUNNING
blog-worker:blog-worker_01 RUNNING
Nếu:
STOPPED
kiểm tra:
sudo supervisorctl tail blog-worker:blog-worker_00
hoặc xem log:
tail -f /var/www/blog/storage/logs/worker.log
Laravel:
php artisan queue:failed
Nếu không có failed jobs:
No failed jobs found.
Nếu có:
ID
Connection
Queue
Class
Failed At
hãy kiểm tra nguyên nhân.
Có thể xem các scheduled tasks bằng:
php artisan schedule:list
Kết quả có thể cho biết:
Command
Frequency
Next Due
Ví dụ:
posts:publish-scheduled Every Minute
stats:update Every Hour
reports:generate Daily
Đây là cách rất hữu ích để kiểm tra xem Scheduler đã nhận configuration chưa.
Khi hệ thống lớn, chúng ta cần biết:
Có bao nhiêu Job đang chờ?
Worker có xử lý kịp không?
Queue có bị nghẽn không?
Laravel có:
queue:monitor
để theo dõi queue vượt ngưỡng.
Ví dụ:
php artisan queue:monitor database:default --max=100
Khi queue vượt ngưỡng, Laravel có thể phát sự kiện QueueBusy để ứng dụng phản ứng phù hợp.
Database Queue phù hợp để học và các ứng dụng nhỏ.
Khi hệ thống lớn, Redis thường được sử dụng làm queue backend:
Laravel
│
▼
Redis
│
▼
Worker
.env có thể cấu hình:
QUEUE_CONNECTION=redis
Kiến trúc:
Laravel
│
▼
Redis
│
├── Job 1
├── Job 2
├── Job 3
└── Job 4
│
▼
Worker
Trong khóa học hiện tại, chúng ta chưa cần triển khai Redis ngay.
Database Queue giúp người mới hiểu bản chất Queue trước.
Nếu sử dụng Redis Queue, Laravel có:
Laravel Horizon
Horizon cung cấp giao diện để theo dõi Queue.
Có thể quan sát:
Jobs
Throughput
Runtime
Failed Jobs
Workers
Horizon đặc biệt hữu ích khi ứng dụng có hệ thống Queue lớn.
Trong Blog CMS cơ bản, Horizon có thể xem là phần nâng cao.
Hai thứ này rất dễ nhầm.
Xử lý:
HTTP Request
Ví dụ:
GET /blog
POST /login
GET /admin/posts
Xử lý:
Background Job
Ví dụ:
Send Email
Generate Report
Process Image
Notification
Mô hình:
HTTP
│
▼
Nginx
│
▼
PHP-FPM
│
▼
Laravel
và:
Queue
│
▼
Worker
│
▼
Job
Là cơ chế của hệ điều hành.
Ví dụ:
* * * * * php artisan schedule:run
Là cơ chế của Laravel.
Ví dụ:
Schedule::command('posts:clean')
->daily();
Kết hợp:
Linux Cron
│
▼
schedule:run
│
▼
Laravel Scheduler
│
▼
Task
Laravel giúp chúng ta quản lý lịch chạy trong source code thay vì phải tạo hàng loạt Cron entry.
.env:
QUEUE_CONNECTION=database
Tạo bảng:
php artisan make:queue-table
php artisan migrate
Chạy Worker:
php artisan queue:work
Production:
Supervisor
↓
queue:work
routes/console.php:
<?php
use Illuminate\Support\Facades\Schedule;
Schedule::command('posts:publish-scheduled')
->everyMinute();
Schedule::command('cleanup:old-data')
->dailyAt('02:00');
Cron:
* * * * * cd /var/www/blog && php artisan schedule:run >> /dev/null 2>&1
Sau Bài 48, Blog CMS của chúng ta đã tiến thêm một bước:
INTERNET
│
▼
┌───────┐
│ Nginx │
└───┬───┘
│
▼
PHP-FPM
│
▼
┌─────────────┐
│ Laravel 13 │
└──────┬──────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
MySQL Storage Cache
│
│
┌───────┴───────┐
│ │
▼ ▼
Queue Scheduler
│ │
▼ ▼
Worker Cron
│
▼
Background
Jobs
Đây là một architecture production cơ bản nhưng rất thực tế.
Trước khi đưa Queue lên production:
[ ] QUEUE_CONNECTION đã cấu hình
[ ] jobs table tồn tại
[ ] failed_jobs table tồn tại
[ ] Job implements ShouldQueue
[ ] queue:work chạy được
[ ] Supervisor đã cài
[ ] Supervisor configuration đúng
[ ] Worker đang RUNNING
[ ] Worker log hoạt động
[ ] queue:failed kiểm tra được
Scheduler:
[ ] Schedule đã định nghĩa
[ ] schedule:list kiểm tra được
[ ] Cron đã cấu hình
[ ] Cron chạy mỗi phút
[ ] Task không bị overlapping ngoài ý muốn
[ ] Scheduler log/error được kiểm tra
Deployment:
[ ] Code mới đã deploy
[ ] composer install
[ ] migrate --force
[ ] optimize
[ ] queue:restart / reload
[ ] Supervisor tự khởi động Worker mới
php artisan make:job TestQueueJob
Cho Job ghi log:
Log::info('Queue Job executed');
Dispatch:
TestQueueJob::dispatch();
Sau đó chạy:
php artisan queue:work
Kiểm tra:
storage/logs/laravel.log
Tạo Job cố tình phát sinh exception:
throw new Exception('Test Queue Error');
Sau đó:
php artisan queue:work --tries=1
Kiểm tra:
php artisan queue:failed
Tạo Schedule:
Schedule::call(function () {
Log::info('Scheduler executed');
})->everyMinute();
Chạy:
php artisan schedule:work
Sau đó kiểm tra:
storage/logs/laravel.log
Cài:
sudo apt install supervisor
Tạo:
/etc/supervisor/conf.d/blog-worker.conf
Khởi động Worker thông qua Supervisor.
Kiểm tra:
sudo supervisorctl status
Thêm:
* * * * * cd /var/www/blog && php artisan schedule:run >> /dev/null 2>&1
Sau đó kiểm tra:
php artisan schedule:list
Sau bài này chúng ta đã hiểu hai khái niệm quan trọng:
QUEUE
và:
SCHEDULER
Queue dùng để xử lý:
Background Jobs
Ví dụ:
Email
Notification
Image Processing
Report
API Sync
Scheduler dùng để xử lý:
Timed Tasks
Ví dụ:
Every Minute
Every Hour
Daily
Weekly
Kiến trúc:
Laravel
│
┌───────┴────────┐
│ │
Queue Scheduler
│ │
▼ ▼
Worker Cron
│ │
▼ ▼
Job Task
Và trong production:
Supervisor
│
▼
Queue Worker
còn:
Cron
│
▼
schedule:run
│
▼
Laravel Scheduler
php artisan queue:work
php artisan queue:restart
php artisan queue:failed
php artisan queue:retry all
php artisan schedule:run
php artisan schedule:work
php artisan schedule:list
Supervisor
↓
Queue Worker
và:
Cron
↓
schedule:run
Quan trọng nhất:
Queue = XỬ LÝ VIỆC NỀN
Scheduler = CHẠY VIỆC THEO LỊCH
Khi kết hợp:
Scheduler
↓
Dispatch Job
↓
Queue
↓
Worker
↓
Background Processing
thì Laravel 13 có thể xử lý được rất nhiều công việc production mà không bắt người dùng phải chờ trong HTTP request.
Bài tiếp theo: Bài 49 — Production Tips
Chúng ta sẽ hoàn thiện phần triển khai Laravel 13 với:
Cache
Optimize
ENV
APP_DEBUG
Security
Performance
Production Configuration
để Blog CMS sẵn sàng bước sang Bài 50 — Tổng kết dự án Blog CMS.
x1
Cập nhật: 2026-09-25T19:54:46.833+07:00
Ở Bài 46, chúng ta đã đưa Laravel 13 lên VPS.
Nhưng một Laravel project trên VPS chưa thể tự nhiên hoạt động như một website production chỉ bằng việc upload source code.
Chúng ta cần một Web Server.
Một mô hình phổ biến là:
Internet
│
▼
Nginx
│
▼
PHP-FPM
│
▼
Laravel 13
│
├── MySQL
├── Storage
└── Cache
Trong bài này chúng ta sẽ học cách cấu hình Nginx cho Laravel 13.
Nginx là một web server và reverse proxy rất phổ biến.
Nó có thể:
nhận HTTP/HTTPS request;
phục vụ file tĩnh;
xử lý domain;
chuyển request PHP cho PHP-FPM;
reverse proxy;
load balancing;
cache;
giới hạn request;
cấu hình HTTPS;
chạy nhiều website trên cùng một VPS.
Trong Laravel deployment, Nginx thường đứng phía trước PHP-FPM.
Ví dụ:
Browser
│
│ HTTPS
▼
Nginx
│
│ FastCGI
▼
PHP-FPM
│
▼
Laravel
Laravel không phải là web server production.
Khi development, chúng ta thường chạy:
php artisan serve
Sau đó truy cập:
http://127.0.0.1:8000
Đây là môi trường phát triển.
Production thường sử dụng:
Nginx
+
PHP-FPM
+
Laravel
Nginx nhận request:
GET /
sau đó đưa request đến Laravel.
Đây là phần rất quan trọng.
Nginx không trực tiếp chạy PHP.
PHP được xử lý bởi:
PHP-FPM
PHP-FPM là:
PHP FastCGI Process Manager
Có thể hình dung:
┌──────────────┐
│ Browser │
└──────┬───────┘
│
▼
┌──────────────┐
│ Nginx │
└──────┬───────┘
│
FastCGI │
▼
┌──────────────┐
│ PHP-FPM │
└──────┬───────┘
│
▼
┌──────────────┐
│ Laravel 13 │
└──────────────┘
Nginx đảm nhiệm phần web server.
PHP-FPM đảm nhiệm việc chạy PHP.
Laravel xử lý application logic.
Laravel 13 yêu cầu:
PHP >= 8.3
Vì vậy server của chúng ta trong khóa học sử dụng:
Ubuntu
Nginx
PHP 8.3
PHP-FPM
MySQL
Composer
Laravel 13
Laravel cũng yêu cầu một số PHP extension như:
Ctype
cURL
DOM
Fileinfo
Filter
Hash
Mbstring
OpenSSL
PCRE
PDO
Session
Tokenizer
XML
Đây là các yêu cầu chính thức của Laravel 13.
Trên VPS:
nginx -v
Ví dụ:
nginx version: nginx/1.24.0
Kiểm tra trạng thái:
sudo systemctl status nginx
Nếu đang chạy:
Active: active (running)
Khởi động:
sudo systemctl start nginx
Cho Nginx tự khởi động cùng server:
sudo systemctl enable nginx
Restart:
sudo systemctl restart nginx
Reload:
sudo systemctl reload nginx
Ví dụ PHP 8.3:
sudo systemctl status php8.3-fpm
Nếu chưa chạy:
sudo systemctl start php8.3-fpm
Enable:
sudo systemctl enable php8.3-fpm
Restart:
sudo systemctl restart php8.3-fpm
Trên Ubuntu, các file thường nằm trong:
/etc/nginx/
Cấu trúc phổ biến:
/etc/nginx/
├── nginx.conf
├── sites-available/
│ └── default
└── sites-enabled/
└── default
Trong đó:
sites-available
chứa configuration của website.
sites-enabled
chứa các website đang được enable.
Giả sử project của chúng ta nằm tại:
/var/www/blog
Laravel có:
/var/www/blog/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
├── routes/
├── storage/
├── vendor/
└── artisan
Điểm quan trọng:
Nginx phải trỏ vào thư mục
public.
Không được trỏ vào:
/var/www/blog
mà phải là:
/var/www/blog/public
Laravel chính thức cũng yêu cầu web server hướng request đến public/index.php, thay vì project root, nhằm tránh expose các file nhạy cảm như .env.
Tạo file:
sudo nano /etc/nginx/sites-available/blog
Nội dung:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/blog/public;
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 32k;
fastcgi_busy_buffers_size 64k;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Đây là cấu hình dựa trên cấu hình Nginx production được Laravel 13 documentation cung cấp, với đường dẫn project và PHP-FPM socket cần thay đổi theo server thực tế.
serverserver {
}
Một server block đại diện cho một website hoặc virtual host.
Ví dụ:
example.com
có thể có một server block.
Website khác:
abc.com
có thể có server block khác.
Do đó một VPS có thể chạy:
VPS
│
├── example.com
│
├── abc.com
│
└── xyz.com
listenlisten 80;
Port:
80
dành cho HTTP.
IPv6:
listen [::]:80;
Khi dùng HTTPS sau này:
listen 443 ssl;
server_nameVí dụ:
server_name example.com www.example.com;
Nginx sẽ biết request dành cho website nào.
Ví dụ browser truy cập:
https://example.com
Nginx tìm server block có:
server_name example.com;
rootĐây là một trong những dòng quan trọng nhất:
root /var/www/blog/public;
Không phải:
root /var/www/blog;
Mà:
root /var/www/blog/public;
Vì Laravel có cấu trúc:
blog/
├── app/
├── config/
├── resources/
├── routes/
├── storage/
├── vendor/
└── public/
├── index.php
├── build/
├── favicon.ico
└── ...
Browser chỉ nên truy cập phần:
public/
Giả sử chúng ta cấu hình:
root /var/www/blog;
Khi đó có nguy cơ expose các file như:
.env
composer.json
composer.lock
artisan
config/
storage/
Đặc biệt .env chứa những thông tin như:
DB_PASSWORD
MAIL_PASSWORD
AWS_SECRET
APP_KEY
Đây là vấn đề bảo mật nghiêm trọng.
Do đó:
❌ /var/www/blog
phải chuyển thành:
✅ /var/www/blog/public
indexindex index.php;
Khi request đến:
/
Nginx biết entry point chính là:
index.php
Laravel sử dụng:
public/index.php
làm entry point của ứng dụng.
location /Phần quan trọng:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
Đây là phần giúp Laravel routing hoạt động.
Ví dụ:
/blog
hoặc:
/blog/laravel-13
không nhất thiết phải là file vật lý.
Nginx sẽ thử:
$uri
Nếu không tồn tại:
$uri/
Nếu vẫn không tồn tại:
/index.php
và giữ query string.
Laravel sau đó nhận request và xử lý:
Route::get('/blog', ...);
hoặc:
Route::get('/blog/{post}', ...);
try_files bằng ví dụGiả sử browser gọi:
/images/logo.png
Nếu file tồn tại:
public/images/logo.png
Nginx phục vụ trực tiếp.
Không cần PHP.
Nhưng nếu gọi:
/blog
không có file:
public/blog
Nginx chuyển request về:
public/index.php
Sau đó Laravel xử lý route.
Ví dụ:
GET /blog
Luồng xử lý:
Browser
│
▼
Nginx
│
├── public/blog tồn tại?
│ │
│ └── Không
│
▼
public/index.php
│
▼
PHP-FPM
│
▼
Laravel Router
│
▼
BlogController
│
▼
Blade
│
▼
HTML
location ~ ^/index\.phpĐây là phần chuyển PHP request cho PHP-FPM:
location ~ ^/index\.php(/|$) {
Laravel 13 documentation sử dụng chính pattern này trong cấu hình Nginx production.
fastcgi_passVí dụ:
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
Nginx nói:
Hãy chuyển PHP request cho PHP-FPM 8.3.
Socket có thể khác tùy server.
Kiểm tra:
ls /var/run/php/
Có thể thấy:
php8.3-fpm.sock
Khi đó:
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
Nếu server sử dụng PHP phiên bản khác thì đường dẫn phải thay đổi tương ứng.
SCRIPT_FILENAMEfastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
Dòng này cho PHP-FPM biết file PHP thực tế cần chạy.
Laravel configuration chính thức cũng sử dụng:
$realpath_root$fastcgi_script_name
trong server block mẫu.
include fastcgi_paramsinclude fastcgi_params;
Nó đưa các FastCGI parameters cần thiết vào request.
Không nên tùy tiện bỏ dòng này khỏi cấu hình.
Cấu hình:
location ~ /\.(?!well-known).* {
deny all;
}
Mục đích là chặn truy cập đến các file hoặc thư mục bắt đầu bằng:
.
Ví dụ:
.env
.git
.gitignore
Trong đó .env đặc biệt quan trọng.
Laravel 13 documentation cũng đưa rule này vào cấu hình Nginx mẫu.
favicon.ico và robots.txtCó thể sử dụng:
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
Hai file này thường được browser hoặc crawler truy cập thường xuyên.
Không cần ghi log những request không đáng kể này.
Sau khi tạo:
/etc/nginx/sites-available/blog
tạo symbolic link:
sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/blog
Có thể xóa site mặc định:
sudo rm /etc/nginx/sites-enabled/default
Đừng restart Nginx ngay.
Trước tiên:
sudo nginx -t
Nếu đúng:
syntax is ok
test is successful
Sau đó:
sudo systemctl reload nginx
Đây là thói quen rất quan trọng.
Luôn:
Sửa config
↓
nginx -t
↓
OK?
↓
reload nginx
Không nên:
Sửa config
↓
restart ngay
nếu chưa kiểm tra syntax.
Giả sử domain:
example.com
DNS cần trỏ:
example.com → IP VPS
Ví dụ:
A
example.com
123.123.123.123
Và:
A
www
123.123.123.123
Sau đó Nginx:
server_name example.com www.example.com;
Nginx cho phép:
VPS
│
├── example.com
│ └── /var/www/example
│
├── blog.com
│ └── /var/www/blog
│
└── shop.com
└── /var/www/shop
Mỗi website có một server block.
Ví dụ:
/etc/nginx/sites-available/
├── example
├── blog
└── shop
và:
/etc/nginx/sites-enabled/
├── example
├── blog
└── shop
Đây là một trong những lý do Nginx rất phù hợp với VPS.
Website production nên sử dụng:
HTTPS
thay vì:
HTTP
Mô hình:
Browser
│
HTTPS :443
▼
Nginx
│
▼
PHP-FPM
│
▼
Laravel
Thông thường có thể sử dụng Let's Encrypt + Certbot để cấp SSL certificate.
Sau khi cấu hình SSL:
http://example.com
có thể redirect sang:
https://example.com
Trong Blog CMS của chúng ta có upload:
Post Image
Laravel public disk mặc định lưu tại:
storage/app/public
Để browser truy cập được, tạo symbolic link:
php artisan storage:link
Khi đó:
storage/app/public
│
│ symbolic link
▼
public/storage
Laravel documentation cũng sử dụng storage:link cho public disk.
Ví dụ database lưu:
posts/abc.jpg
thì URL có thể là:
Storage::url($post->image)
hoặc:
asset('storage/' . $post->image)
Blog CMS của chúng ta có upload image.
Nginx cũng có giới hạn kích thước request.
Ví dụ:
client_max_body_size 10M;
Nếu website cho phép upload file lớn hơn mặc định, có thể cấu hình:
server {
...
client_max_body_size 10M;
}
Sau đó:
sudo nginx -t
và:
sudo systemctl reload nginx
Tuy nhiên còn phải kiểm tra giới hạn của PHP:
upload_max_filesize
post_max_size
Ví dụ:
upload_max_filesize = 10M
post_max_size = 12M
Không nên chỉ sửa Nginx mà quên PHP.
Nếu upload file quá lớn, Nginx có thể trả:
413 Request Entity Too Large
Nguyên nhân thường liên quan:
client_max_body_size
Ví dụ:
client_max_body_size 2M;
nhưng người dùng upload:
5MB
Nginx có thể chặn request trước khi Laravel nhận được.
Log thường nằm trong:
/var/log/nginx/
Ví dụ:
access.log
error.log
Xem error log:
sudo tail -f /var/log/nginx/error.log
Xem access log:
sudo tail -f /var/log/nginx/access.log
Khi website gặp lỗi:
502
403
404
413
hãy kiểm tra log.
Một lỗi rất phổ biến:
502 Bad Gateway
Nếu:
Nginx
│
X
PHP-FPM
thì Nginx không thể giao tiếp với PHP-FPM.
Kiểm tra:
sudo systemctl status php8.3-fpm
Kiểm tra socket:
ls /var/run/php/
Ví dụ socket thực tế là:
php8.3-fpm.sock
nhưng Nginx lại cấu hình:
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
thì sẽ lỗi.
Nếu gặp:
403 Forbidden
có thể kiểm tra:
quyền thư mục;
owner;
Nginx configuration;
public/;
file permissions.
Laravel cần web server có quyền ghi vào:
storage/
và:
bootstrap/cache/
Laravel deployment documentation cũng yêu cầu các thư mục này phải writable bởi web server process.
Nếu:
/blog
trả:
404
hãy kiểm tra:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
Nếu thiếu:
/index.php
Laravel routing có thể không hoạt động đúng.
Sau đó kiểm tra route:
php artisan route:list
Nếu Laravel chạy nhưng:
CSS
JS
Image
không hiển thị, kiểm tra:
public/build
Nếu dùng Vite, trên server cần build:
npm install
npm run build
Sau đó kiểm tra:
public/build/
Laravel không tự động biến source frontend thành production assets chỉ vì Nginx được cấu hình.
Một hiểu nhầm phổ biến:
Nginx = Laravel
Không đúng.
Nginx chỉ là web server.
Kiến trúc:
Nginx
↓
PHP-FPM
↓
Laravel
↓
Application
↓
Database
Mỗi thành phần có nhiệm vụ riêng.
Một server Laravel production hoàn chỉnh có thể nhìn như sau:
INTERNET
│
▼
┌─────────┐
│ Nginx │
└────┬────┘
│
▼
┌───────────┐
│ PHP-FPM │
└─────┬─────┘
│
▼
┌─────────────┐
│ Laravel 13 │
└──────┬──────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
MySQL Storage Cache
Đây chính là kiến trúc cơ bản mà chúng ta đang xây dựng cho Blog CMS.
Một configuration tương đối hoàn chỉnh:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/blog/public;
index index.php;
charset utf-8;
client_max_body_size 10M;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 32k;
fastcgi_busy_buffers_size 64k;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Laravel 13's official deployment example also includes security headers such as X-Frame-Options and X-Content-Type-Options, the Laravel try_files rule, PHP-FPM configuration and hidden-file protection.
Quy trình:
sudo nginx -t
Nếu:
syntax is ok
test is successful
thì:
sudo systemctl reload nginx
Kiểm tra:
sudo systemctl status nginx
Sau đó mở:
https://example.com
Trước khi xem deployment là hoàn thành:
[ ] PHP >= 8.3
[ ] PHP-FPM đang chạy
[ ] Nginx đang chạy
[ ] root trỏ tới /public
[ ] server_name đúng domain
[ ] try_files đúng
[ ] fastcgi_pass đúng PHP-FPM socket
[ ] SCRIPT_FILENAME đúng
[ ] .env không public
[ ] storage/ có quyền ghi
[ ] bootstrap/cache có quyền ghi
[ ] php artisan storage:link
[ ] npm run build
[ ] nginx -t thành công
[ ] nginx reload
[ ] HTTPS hoạt động
[ ] APP_DEBUG=false
Kiểm tra Nginx:
nginx -v
Kiểm tra PHP-FPM:
sudo systemctl status php8.3-fpm
Tìm PHP-FPM socket:
ls /var/run/php/
Tạo server block:
/etc/nginx/sites-available/blog
và cấu hình Laravel:
/var/www/blog/public
Kiểm tra:
sudo nginx -t
Reload:
sudo systemctl reload nginx
Mở Blog CMS và kiểm tra:
/
├── Login
├── Register
├── Blog
├── Category
├── Post
└── Admin
Upload một hình ảnh vào Blog CMS.
Kiểm tra:
storage/app/public
và:
public/storage
Sau bài này chúng ta đã hiểu được:
Nginx
là gì.
PHP-FPM
là gì.
Và quan trọng nhất:
Nginx
↓
PHP-FPM
↓
Laravel 13
Chúng ta đã biết:
cài và kiểm tra Nginx;
kiểm tra PHP-FPM;
tạo Server Block;
cấu hình server_name;
cấu hình root;
trỏ Nginx vào public/;
cấu hình try_files;
cấu hình PHP-FPM;
bảo vệ file ẩn;
cấu hình upload;
cấu hình domain;
chạy nhiều website trên một VPS;
đọc Nginx log;
xử lý lỗi 403;
xử lý lỗi 404;
xử lý lỗi 502;
xử lý lỗi 413;
kết hợp Nginx với Laravel Storage;
kiểm tra configuration bằng nginx -t;
reload Nginx.
Kiến trúc chúng ta đang hướng tới:
DOMAIN
│
▼
┌──────────┐
│ Nginx │
└────┬─────┘
│
▼
┌───────────┐
│ PHP-FPM │
└─────┬─────┘
│
▼
┌─────────────┐
│ Laravel 13 │
└──────┬──────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
MySQL Storage Cache
Đây là nền tảng rất quan trọng trước khi chúng ta đi tiếp vào Queue Worker và Scheduler.
Có 5 điều đặc biệt quan trọng:
Nginx → PHP-FPM → Laravel
publicroot /var/www/blog/public;
try_filestry_files $uri $uri/ /index.php?$query_string;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
sudo nginx -t
sudo systemctl reload nginx
Bài tiếp theo: Bài 48 — Queue Worker & Scheduler
Chúng ta sẽ đưa những phần chạy nền của Blog CMS lên production:
Laravel Queue
│
▼
Queue Worker
│
├── Send Mail
├── Notification
├── Job
└── Background Task
Scheduler
│
├── Cron
├── Daily Task
├── Cleanup
└── Scheduled Job
Đây là bước biến một Laravel project từ website đơn giản thành một ứng dụng production thực tế.
x1
Cập nhật: 2026-09-24T20:25:24.273+07:00
Ở Bài 45, chúng ta đã đưa Laravel 13 lên Shared Hosting.
Trong bài này, chúng ta tiến thêm một bước:
Shared Hosting
↓
VPS
VPS cho phép chúng ta tự quản lý server ở mức thấp hơn:
VPS
│
├── Ubuntu
├── Nginx
├── PHP
├── PHP-FPM
├── MySQL
├── Composer
├── Git
├── SSL
└── Laravel 13
Đây là mô hình rất quan trọng nếu muốn làm Laravel ở môi trường production thực tế
VPS là:
Virtual Private Server
Hiểu đơn giản, VPS là một máy chủ ảo có:
CPU
RAM
SSD
hệ điều hành
IP riêng
quyền quản trị server
Ví dụ:
Internet
│
▼
Public IP
│
▼
VPS
│
├── Nginx
├── PHP 8.3
├── MySQL
└── Laravel 13
Khác với Shared Hosting:
Shared Hosting
→ Nhà cung cấp quản lý phần lớn server
còn:
VPS
→ Bạn quản lý phần lớn server
| Shared Hosting | VPS |
|---|---|
| Dễ sử dụng | Khó hơn |
| Control Panel | SSH/Terminal |
| Ít quyền hệ thống | Nhiều quyền |
| Ít phải cấu hình | Tự cấu hình |
| Tài nguyên chia sẻ | Tài nguyên VPS riêng |
| Phù hợp website nhỏ | Phù hợp ứng dụng cần kiểm soát server |
| Không cần biết Linux quá sâu | Cần biết Linux |
| Có thể giới hạn Composer/CLI | Chủ động Composer/CLI |
Có thể hình dung:
Shared Hosting
↓
"Thuê một căn phòng"
VPS
↓
"Thuê một căn nhà và tự quản lý"
Đối với Blog CMS Laravel 13, một VPS cơ bản có thể gồm:
Ubuntu
↓
Nginx
↓
PHP 8.3+
↓
PHP-FPM
↓
Laravel 13
↓
MySQL
Ngoài ra:
Git
Composer
Node.js
npm
Certbot
cũng rất hữu ích.
Laravel 13 yêu cầu tối thiểu PHP 8.3 và một số PHP extension như Ctype, cURL, DOM, Fileinfo, Mbstring, OpenSSL, PDO, Session, Tokenizer và XML.
Sau khi hoàn thành bài này, server sẽ có dạng:
INTERNET
│
▼
example.com
│
▼
Nginx
│
▼
public/index.php
│
▼
Laravel 13
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
MySQL Storage Cache
Nginx nhận request:
https://example.com
sau đó chuyển request PHP tới:
PHP-FPM
và Laravel xử lý request.
Giả sử chúng ta thuê một VPS Linux mới.
Ví dụ:
OS:
Ubuntu
RAM:
2 GB
CPU:
2 vCPU
Disk:
40 GB SSD
Public IP:
203.0.113.10
Đây chỉ là ví dụ.
Thông số thực tế tùy project.
SSH là phương thức kết nối từ máy tính tới server.
Ví dụ trên Windows:
ssh root@203.0.113.10
Trong đó:
ssh
↓
giao thức SSH
root
↓
username
203.0.113.10
↓
IP VPS
Sau khi đăng nhập:
local computer
│
│ SSH
▼
VPS
Ban đầu nhà cung cấp VPS có thể cho:
root
Nhưng không nên sử dụng root cho mọi thao tác hàng ngày.
Tạo user riêng:
adduser deploy
Sau đó cho user quyền sudo:
usermod -aG sudo deploy
Kiểm tra:
groups deploy
Kết quả có:
sudo
User:
deploy
sẽ được dùng để quản lý application thay vì đăng nhập root liên tục.
Thay vì chỉ dùng password:
SSH Password
có thể sử dụng:
SSH Key
Mô hình:
Computer
│
├── private key
└── public key
│
▼
VPS
Private key:
Không được chia sẻ.
Public key:
Có thể đưa lên server.
Sau khi đăng nhập:
sudo apt update
sau đó:
sudo apt upgrade -y
Mục đích:
Package list
↓
Update
↓
Security patches
↓
System packages
Không nên bỏ qua việc cập nhật server.
lsb_release -a
hoặc:
cat /etc/os-release
Ví dụ:
Ubuntu
24.04 LTS
Tên phiên bản không quan trọng bằng việc:
Hệ điều hành phải còn được hỗ trợ và có package phù hợp với PHP 8.3+.
sudo apt install nginx -y
Kiểm tra:
systemctl status nginx
Nếu thấy:
active (running)
Nginx đang chạy.
Mở browser:
http://203.0.113.10
Nếu thấy trang mặc định Nginx:
Welcome to nginx!
thì Nginx đã hoạt động.
Nginx là web server.
Request:
Browser
│
▼
Nginx
│
▼
PHP-FPM
│
▼
Laravel
Ví dụ:
GET /blog
Nginx nhận request.
Sau đó request được chuyển cho Laravel xử lý.
Laravel 13 yêu cầu:
PHP >= 8.3
Kiểm tra PHP:
php -v
Nếu server có PHP 8.3:
PHP 8.3.x
là đạt yêu cầu tối thiểu.
PHP-FPM:
PHP FastCGI Process Manager
Nginx không trực tiếp chạy PHP như:
Nginx → PHP source
Mà thường:
Browser
↓
Nginx
↓
PHP-FPM
↓
PHP
↓
Laravel
Ví dụ socket:
/var/run/php/php8.3-fpm.sock
Laravel 13 deployment documentation cũng sử dụng PHP-FPM trong cấu hình Nginx mẫu.
Laravel 13 cần các extension cần thiết.
Ví dụ Ubuntu:
sudo apt install -y \
php8.3-fpm \
php8.3-mysql \
php8.3-curl \
php8.3-mbstring \
php8.3-xml \
php8.3-zip \
php8.3-bcmath \
php8.3-intl
Sau đó:
php -m
Kiểm tra các extension.
Lưu ý:
Tên package PHP có thể khác tùy phiên bản Ubuntu/repository.
Không nên copy một lệnh cài PHP từ một server Ubuntu khác mà không kiểm tra repository của server hiện tại.
sudo systemctl status php8.3-fpm
Nếu:
active (running)
thì PHP-FPM đang hoạt động.
Khởi động cùng hệ thống:
sudo systemctl enable php8.3-fpm
sudo apt install mysql-server -y
Kiểm tra:
sudo systemctl status mysql
Nếu:
active (running)
MySQL đang hoạt động.
Đăng nhập MySQL:
sudo mysql
Tạo database:
CREATE DATABASE blog
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
Tạo user:
CREATE USER 'blog_user'@'localhost'
IDENTIFIED BY 'StrongPasswordHere';
Cấp quyền:
GRANT ALL PRIVILEGES
ON blog.*
TO 'blog_user'@'localhost';
Sau đó:
FLUSH PRIVILEGES;
Thoát:
EXIT;
.envKhông nên:
DB_USERNAME=root
cho application production nếu không cần thiết.
Nên:
DB_DATABASE=blog
DB_USERNAME=blog_user
DB_PASSWORD=********
Mô hình:
Laravel
│
▼
blog_user
│
▼
blog database
giúp giới hạn quyền của application.
Kiểm tra:
composer --version
Nếu chưa có Composer thì cài theo hướng dẫn chính thức của Composer.
Sau khi cài:
composer --version
Ví dụ:
Composer version 2.x
sudo apt install git -y
Kiểm tra:
git --version
Git giúp chúng ta triển khai:
GitHub
↓
VPS
↓
Laravel
thay vì phải upload ZIP thủ công mỗi lần cập nhật.
Ví dụ:
sudo mkdir -p /var/www/blog
Gán owner:
sudo chown -R deploy:deploy /var/www/blog
Bây giờ:
/var/www/blog
là nơi chứa Laravel application.
Nếu source nằm trên GitHub:
cd /var/www
sau đó:
git clone https://github.com/username/blog.git
Kết quả:
/var/www/blog
có:
app/
bootstrap/
config/
database/
public/
resources/
routes/
.envRepository:
blog/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── resources/
├── routes/
├── .env.example
└── composer.json
Không nên commit:
.env
Production .env phải được tạo trực tiếp trên server.
.envcd /var/www/blog
Copy:
cp .env.example .env
Sau đó:
nano .env
Ví dụ:
APP_NAME="Blog CMS"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://example.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=blog_user
DB_PASSWORD=StrongPasswordHere
Nếu đây là project mới trên server:
php artisan key:generate
Nếu project đã có dữ liệu production và đã sử dụng một APP_KEY trước đó:
Không được tự ý tạo key mới.
Đối với project đang triển khai lần đầu:
php artisan key:generate
sẽ tạo:
APP_KEY=base64:...
Trong project:
cd /var/www/blog
chạy:
composer install \
--no-dev \
--optimize-autoloader
Mục đích:
composer.json
+
composer.lock
↓
vendor/
Sau đó:
vendor/
được tạo.
Nếu server dùng để build frontend:
npm install
Sau đó:
npm run build
Kết quả:
public/
└── build/
Tuy nhiên production server không nhất thiết phải giữ:
node_modules/
nếu bạn đã build asset ở CI/local và chỉ deploy public/build.
Sau khi .env đã cấu hình:
php artisan migrate --force
Laravel sẽ tạo database schema:
users
categories
posts
personal_access_tokens
...
Đối với Blog CMS của chúng ta, đây là lúc toàn bộ migration đã học từ đầu khóa học được đưa lên VPS.
Nếu cần dữ liệu ban đầu:
php artisan db:seed --force
Ví dụ:
categories
admin user
default settings
Nhưng tuyệt đối phải kiểm tra Seeder trước khi chạy production.
Không chạy Seeder có thao tác kiểu:
Model::truncate();
nếu database đã có dữ liệu thật.
Tạo symbolic link:
php artisan storage:link
Mô hình:
public/storage
│
▼
storage/app/public
Blog CMS có upload image nên bước này rất quan trọng.
Laravel deployment/filesystem documentation sử dụng public/storage để truy cập các file của public disk.
Laravel cần quyền ghi vào:
storage/
và:
bootstrap/cache/
Laravel 13 deployment documentation yêu cầu web server process có quyền ghi vào hai thư mục này.
Có thể thiết lập owner phù hợp:
sudo chown -R deploy:www-data /var/www/blog
Sau đó:
sudo chmod -R ug+rwx \
/var/www/blog/storage \
/var/www/blog/bootstrap/cache
Không nên giải quyết permission bằng:
chmod -R 777 /var/www/blog
Đây là phần quan trọng nhất của bài.
Tạo:
sudo nano /etc/nginx/sites-available/blog
Ví dụ:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/blog/public;
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME
$realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Đây là cấu hình dựa trên cấu hình Nginx deployment mà Laravel cung cấp cho Laravel 13. Điểm quan trọng là root phải là /public và request được chuyển về public/index.php.
root phải là /public?Sai:
root /var/www/blog;
Đúng:
root /var/www/blog/public;
Vì Laravel có:
/var/www/blog/
│
├── .env
├── artisan
├── composer.json
├── app/
├── config/
└── public/
└── index.php
Nếu web root là:
/var/www/blog
thì server có thể expose những file không nên public.
Laravel cũng cảnh báo không phục vụ application từ project root vì có thể làm lộ các file cấu hình nhạy cảm.
Tạo symbolic link:
sudo ln -s \
/etc/nginx/sites-available/blog \
/etc/nginx/sites-enabled/blog
Nếu muốn xóa site mặc định:
sudo rm /etc/nginx/sites-enabled/default
Sau đó kiểm tra:
sudo nginx -t
Nếu:
syntax is ok
test is successful
thì reload:
sudo systemctl reload nginx
Giả sử VPS có IP:
203.0.113.10
DNS:
example.com
tạo:
A
@
203.0.113.10
và:
A
www
203.0.113.10
Mô hình:
example.com
│
▼
DNS
│
▼
203.0.113.10
│
▼
Nginx
│
▼
/var/www/blog/public
Trước khi cấu hình domain hoàn chỉnh, có thể kiểm tra:
http://203.0.113.10
Nếu Nginx đã nhận đúng server block và DNS/domain được cấu hình phù hợp, request sẽ đi vào Laravel.
Production nên sử dụng:
https://example.com
thay vì:
http://example.com
Một lựa chọn phổ biến trên Ubuntu + Nginx là Certbot.
Sau khi cài Certbot phù hợp với hệ điều hành:
sudo certbot --nginx
Certbot có thể cấu hình certificate và HTTPS cho Nginx.
Sau đó kiểm tra:
https://example.com
.env ProductionCuối cùng:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
Đặc biệt:
APP_DEBUG=false
Laravel cảnh báo không bật debug trong production vì có thể làm lộ thông tin cấu hình nhạy cảm.
Sau khi application đã hoạt động:
php artisan optimize
Laravel 13 cung cấp optimize để cache các thành phần production như configuration, events, routes và views.
Có thể kiểm tra:
php artisan optimize:clear
khi cần xóa cache trong quá trình troubleshooting.
Mở:
https://example.com
Sau đó test:
/
/login
/register
/blog
/admin/users
/api/posts
và:
/up
Laravel 13 có health route /up, trả HTTP 200 nếu application boot thành công và HTTP 500 nếu application gặp exception khi boot.
Trên VPS:
php artisan migrate:status
Ví dụ:
Migration name Ran?
------------------------------------------------
create_users_table Yes
create_categories_table Yes
create_posts_table Yes
create_personal_access_tokens_table Yes
Nếu tất cả:
Yes
database schema đã được triển khai.
Nếu Blog CMS sử dụng queue:
php artisan queue:work
có thể chạy thử.
Nhưng:
Không nên dùng terminal SSH để chạy worker production lâu dài.
Nếu SSH đóng:
queue worker
↓
process kết thúc
Do đó production cần process monitor.
Phần này sẽ được học kỹ hơn ở:
Bài 48 — Queue Worker & Scheduler
Laravel:
storage/logs/
Ví dụ:
storage/logs/laravel.log
Có thể xem:
tail -f storage/logs/laravel.log
Khi website:
500
đừng đoán.
Hãy kiểm tra:
Laravel log
Nginx log
PHP-FPM log
Log thường nằm trong:
/var/log/nginx/
Ví dụ:
sudo tail -f /var/log/nginx/error.log
và:
sudo tail -f /var/log/nginx/access.log
Mô hình debug:
Browser
↓
Nginx
↓
PHP-FPM
↓
Laravel
↓
MySQL
Phải xác định lỗi nằm ở tầng nào.
Nếu Nginx báo:
502 Bad Gateway
kiểm tra PHP-FPM:
sudo systemctl status php8.3-fpm
Sau đó:
sudo journalctl -u php8.3-fpm
Một nguyên nhân thường gặp:
Nginx
↓
socket PHP-FPM
↓
SAI đường dẫn
Ví dụ Nginx:
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
nhưng server thực tế dùng socket khác.
Kiểm tra:
ls /var/run/php/
Nếu:
502 Bad Gateway
không có nghĩa Laravel chắc chắn bị lỗi.
Có thể là:
Nginx
↓
PHP-FPM
X
Kiểm tra:
systemctl status php8.3-fpm
và:
ls /var/run/php/
Sau đó đối chiếu:
fastcgi_pass ...
Kiểm tra:
Document Root
phải là:
/var/www/blog/public
không phải:
/var/www/blog
Sau đó kiểm tra:
Permission
Owner
Nginx configuration
Nếu:
/
chạy được nhưng:
/blog
404:
kiểm tra:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
Đây là phần giúp request không phải file tĩnh được chuyển vào Laravel.
Ví dụ:
/blog
↓
Nginx
↓
index.php
↓
Laravel Router
↓
BlogController
Ví dụ:
file_put_contents(...):
Permission denied
Kiểm tra:
ls -la storage
và:
ls -la bootstrap/cache
Sau đó đảm bảo web server có quyền ghi.
Laravel yêu cầu quyền ghi cho hai thư mục:
storage/
bootstrap/cache/
Sau khi triển khai:
VPS
│
├── /var/www/blog
│ │
│ ├── app/
│ ├── bootstrap/
│ ├── config/
│ ├── database/
│ ├── public/
│ │ ├── index.php
│ │ ├── build/
│ │ └── storage
│ ├── resources/
│ ├── routes/
│ ├── storage/
│ ├── vendor/
│ ├── .env
│ ├── artisan
│ └── composer.json
│
├── Nginx
│
├── PHP-FPM
│
└── MySQL
Tóm lại:
1. Tạo VPS
↓
2. SSH
↓
3. Update Ubuntu
↓
4. Tạo deploy user
↓
5. Cài Nginx
↓
6. Cài PHP 8.3+
↓
7. Cài PHP-FPM
↓
8. Cài MySQL
↓
9. Cài Composer
↓
10. Cài Git
↓
11. Clone project
↓
12. Tạo .env
↓
13. composer install
↓
14. npm run build
↓
15. migrate
↓
16. storage:link
↓
17. Permission
↓
18. Nginx
↓
19. DNS
↓
20. HTTPS
↓
21. optimize
↓
22. Test
Một trong những ưu điểm lớn của VPS là có thể triển khai bằng Git.
Ví dụ lần đầu:
cd /var/www
git clone https://github.com/username/blog.git
cd blog
composer install --no-dev --optimize-autoloader
npm install
npm run build
php artisan migrate --force
php artisan storage:link
php artisan optimize
Lần cập nhật sau:
cd /var/www/blog
git pull
composer install --no-dev --optimize-autoloader
npm install
npm run build
php artisan migrate --force
php artisan optimize
Đây đã bắt đầu giống một quy trình deployment thực tế.
git pull mù quángProduction không phải nơi để:
code thử
↓
git pull
↓
hy vọng
Quy trình tốt hơn:
LOCAL
↓
Code
↓
Test
↓
Commit
↓
Push
↓
Git
↓
VPS
↓
Deploy
↓
Test
Đặc biệt khi có:
Database migration
Config change
Dependency change
phải biết chính xác deployment đang thay đổi điều gì.
Ở bài đầu tiên về VPS, chúng ta tập trung vào:
VPS
Nginx
PHP-FPM
MySQL
Laravel
HTTPS
Chưa cần xây dựng:
Load Balancer
Multiple VPS
Blue/Green Deployment
CI/CD phức tạp
Kubernetes
Đó là những vấn đề nâng cao.
Mục tiêu của bài:
Có thể tự triển khai một Laravel 13 Blog CMS lên một VPS và hiểu từng thành phần đang làm gì.
Sau hai bài 45 và 46:
Bài 45
Shared Hosting
và:
Bài 46
VPS
chúng ta có:
DEPLOY LARAVEL
│
┌────────┴────────┐
│ │
Shared Hosting VPS
│ │
Control Panel SSH
│ │
Ít quyền hệ thống Toàn quyền hơn
│ │
Dễ triển khai Tự quản lý
VPS phù hợp khi bạn cần:
SSH
Composer
Cron
Queue Worker
Redis
Supervisor
Nginx
Custom PHP
Background Process
Đặc biệt:
Laravel Queue
Scheduler
API
WebSocket
sẽ cần nhiều quyền kiểm soát server hơn.
Không phải website nào cũng cần VPS.
Ví dụ:
Blog nhỏ
Website giới thiệu
Landing Page
CMS nhỏ
có thể chạy tốt trên Shared Hosting.
Không nên nghĩ:
VPS = luôn tốt hơn
Mà nên nghĩ:
Nhu cầu
↓
Kiến trúc
↓
Loại hosting
Tạo một VPS Linux.
Cài:
Nginx
PHP 8.3+
PHP-FPM
MySQL
Composer
Git
Tạo:
/var/www/blog
và clone Blog CMS.
Cấu hình:
.env
với:
APP_ENV=production
APP_DEBUG=false
Tạo database:
blog
và user:
blog_user
Chạy:
composer install --no-dev --optimize-autoloader
Chạy:
php artisan migrate --force
Chạy:
php artisan storage:link
Cấu hình Nginx:
/var/www/blog/public
làm web root.
Trỏ domain về VPS.
Cài HTTPS.
Kiểm tra:
https://example.com
và:
https://example.com/up
Virtual Private Server
Remote Server Management
Web Server
PHP Process Manager
Database
PHP Dependency Manager
Source Code Management
project/public
APP_DEBUG=false
storage/
bootstrap/cache/
php artisan optimize
Ở Bài 45:
Laravel 13
↓
Shared Hosting
Ở Bài 46:
Laravel 13
↓
VPS
↓
Ubuntu
↓
Nginx
↓
PHP-FPM
↓
MySQL
Chúng ta đã biết cách:
SSH vào VPS
↓
Cài môi trường
↓
Đưa source lên server
↓
Cấu hình .env
↓
Cấu hình Database
↓
Composer
↓
Migration
↓
Storage
↓
Nginx
↓
DNS
↓
HTTPS
↓
Optimize
↓
Laravel 13 chạy production
Điểm quan trọng nhất cần nhớ:
Laravel 13
│
▼
/var/www/blog
│
▼
public
│
▼
Nginx
│
▼
PHP-FPM
│
▼
Laravel
│
▼
MySQL
Và không được cấu hình Nginx phục vụ project root:
❌ /var/www/blog
mà phải:
✅ /var/www/blog/public
Laravel 13 cũng yêu cầu PHP 8.3 trở lên, đồng thời storage và bootstrap/cache phải writable đối với web server process.
Chúng ta đã biết VPS gồm những thành phần nào và cách đưa Laravel 13 lên VPS.
Nhưng trong bài này chúng ta mới sử dụng Nginx ở mức cấu hình cơ bản.
Bài tiếp theo sẽ đi sâu vào chính thành phần này:
Chúng ta sẽ học:
Nginx là gì?
↓
Server Block
↓
Document Root
↓
location
↓
try_files
↓
PHP-FPM
↓
FastCGI
↓
Domain
↓
HTTPS
↓
Laravel 13
và quan trọng nhất là hiểu từng dòng trong file cấu hình Nginx Laravel, thay vì chỉ copy một đoạn config rồi chạy.
x1
Cập nhật: 2026-09-22T15:58:04.904+07:00