Hướng dẫn sử dụng `$request` trong Laravel
Mọi thứ người dùng gửi lên (tham số trên URL, dữ liệu form, JSON, file upload, cookie, header) đều được Laravel gom vào một object duy nhất: `Illuminate\Http\Request`, thường được đặt tên là `$request`. Bài này đi qua những thứ bạn sẽ dùng hằng ngày: lấy dữ liệu, ép kiểu, validate, upload file, session, cookie, header, và các lỗi hay gặp.
1. Lấy $request ở đâu?
Có 3 cách:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller
{
// Cách 1 (nên dùng): khai báo kiểu Request, Laravel tự truyền vào.
public function index(Request $request)
{
$q = $request->input('q');
}
// Có tham số route? Request đặt ở đâu cũng được, Laravel nhận theo kiểu.
public function update(Request $request, string $id)
{
// ...
}
}
<?php
// Cách 2: trong route closure
use Illuminate\Http\Request;
Route::get('/search', function (Request $request) {
return $request->query('q');
});
// Cách 3: helper request(), dùng được ở bất cứ đâu (view, helper...)
$q = request('q'); // giống request()->input('q')
$ip = request()->ip();
Ưu tiên cách 1: phụ thuộc hiện rõ trên chữ ký hàm, dễ đọc và dễ test.
2. Lấy dữ liệu đầu vào
2.1. input(): lấy từ mọi nguồn
<?php
$title = $request->input('title'); // null nếu không có
$page = $request->input('page', 1); // giá trị mặc định
$all = $request->all(); // toàn bộ input + file
input() đọc từ query string, form và JSON body cùng lúc. Gửi JSON bằng fetch hay Postman? input() tự xử lý khi header là Content-Type: application/json, không cần tự giải mã.
2.2. Tách riêng nguồn khi cần
<?php
$request->query('page'); // CHỈ từ query string (?page=2)
$request->post('title'); // CHỈ từ form body
$request->json('title'); // CHỈ từ JSON body
Nếu một giá trị phải đến từ URL (ví dụ page khi phân trang), dùng query() cho rõ ràng, để người dùng không gửi nó qua đường khác.
2.3. Dữ liệu lồng nhau: dùng dấu chấm
Với form có name="address[city]" hoặc JSON {"items":[{"name":"Bút"}]}:
<?php
$city = $request->input('address.city');
$first = $request->input('items.0.name'); // "Bút"
$names = $request->input('items.*.name'); // ["Bút", ...]
2.4. Lấy một phần
<?php
$data = $request->only(['title', 'content']); // chỉ lấy các key này
$data = $request->except(['_token', 'password']); // lấy tất cả trừ các key này
2.5. Truy cập kiểu thuộc tính
<?php
$title = $request->title; // tương đương input('title')
Ngắn, nhưng nếu route có tham số trùng tên thì dễ nhầm. Trong code nghiêm túc, input() rõ ràng hơn.
3. Ép kiểu dữ liệu
Mọi giá trị từ form/URL đều là chuỗi. Laravel có sẵn các hàm ép kiểu an toàn:
<?php
$page = $request->integer('page', 1); // int; mặc định 1 khi KHÔNG có key
$price = $request->float('price'); // float
$active = $request->boolean('is_active'); // true với "1", "true", "on", "yes"
$name = $request->string('name')->trim(); // Stringable, nối tiếp được
$from = $request->date('from', 'Y-m-d'); // Carbon hoặc null
$tags = $request->collect('tags'); // Collection
Cẩn thận: giá trị mặc định chỉ dùng khi không có key. Với ?page=abc, integer('page', 1) trả về 0 (giống (int) 'abc'), không phải 1. Số âm như ?page=-5 cũng lọt qua. Vì vậy với số do người dùng nhập, vẫn nên chặn khoảng giá trị (max(1, ...)) hoặc validate bằng rule integer|min:1.
boolean() đặc biệt hữu ích với checkbox: checkbox không được tick thì không được gửi lên, nên key không tồn tại. $request->boolean('is_active') trả về false trong trường hợp đó, không cần kiểm tra thêm.
Với enum (PHP 8.1+):
<?php
enum PostStatus: string
{
case Draft = 'draft';
case Published = 'published';
}
$status = $request->enum('status', PostStatus::class); // PostStatus hoặc null nếu giá trị lạ
4. Kiểm tra key có tồn tại không
<?php
$request->has('name'); // key có mặt (kể cả rỗng)
$request->has(['name', 'email']); // có mặt TẤT CẢ
$request->hasAny(['name', 'email']); // có mặt ÍT NHẤT MỘT
$request->filled('name'); // có mặt VÀ không rỗng
$request->missing('name'); // không có mặt
// Chạy callback chỉ khi có giá trị
$request->whenFilled('q', function (string $q) use ($query) {
$query->where('title', 'like', "%{$q}%");
});
Bẫy: chuỗi rỗng thành null
Laravel mặc định bật hai middleware:
TrimStrings: cắt khoảng trắng hai đầu mọi chuỗi.ConvertEmptyStringsToNull: chuỗi rỗng""thànhnull.
Vì vậy ô input để trống sẽ là null, không phải "". Đây là lý do rule validate thường có nullable, và cột database tương ứng nên cho phép NULL.
5. Validate: phần quan trọng nhất
5.1. $request->validate() trong controller
<?php
public function store(Request $request)
{
$data = $request->validate([
'title' => ['required', 'string', 'max:200'],
'content' => ['required', 'string'],
'email' => ['nullable', 'email'],
'is_active' => ['boolean'],
]);
// Tới được dòng này nghĩa là dữ liệu đã hợp lệ.
// $data CHỈ chứa các key đã khai báo rule.
$post = $request->user()->posts()->create($data);
return redirect()->route('posts.show', $post)->with('status', 'Đã đăng bài');
}
Khi validate thất bại, Laravel tự xử lý:
- Request từ form thường: chuyển hướng về trang trước, kèm lỗi (
$errorstrong Blade) và dữ liệu cũ (old('title')). - Request muốn nhận JSON (header
Accept: application/json): trả về HTTP 422 với danh sách lỗi.
Bạn không phải viết if cho từng field, tự gom danh sách lỗi hay tự giữ lại giá trị form.
5.2. Form Request: tách validate ra class riêng
Khi rule dài hoặc dùng lại nhiều nơi:
php artisan make:request StorePostRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
// Ai được gửi request này? Trả false => 403.
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:200'],
'content' => ['required', 'string'],
'thumbnail' => ['nullable', 'image', 'mimes:jpg,png,webp', 'max:2048'], // KB
];
}
// Chuẩn hóa dữ liệu TRƯỚC khi validate.
protected function prepareForValidation(): void
{
$this->merge([
'title' => strip_tags((string) $this->input('title')),
]);
}
}
Controller trở nên rất gọn:
<?php
use App\Http\Requests\StorePostRequest;
public function store(StorePostRequest $request)
{
// Validate + authorize đã chạy xong TRƯỚC khi vào hàm này.
$post = $request->user()->posts()->create($request->validated());
return redirect()->route('posts.show', $post);
}
5.3. Đừng bao giờ create($request->all())
<?php
// SAI: người dùng gửi thêm is_admin=1 hay user_id=999 là lọt vào DB
// (nếu model không chặn bằng $fillable).
Post::create($request->all());
// ĐÚNG: chỉ lấy những field đã qua validate.
Post::create($request->validated());
// Hoặc lấy một phần của dữ liệu đã validate.
Post::create($request->safe()->only(['title', 'content']));
6. Upload file: $request->file()
Mỗi file upload là một object UploadedFile, có sẵn hàm kiểm tra và lưu:
<?php
public function updateAvatar(Request $request)
{
$request->validate([
// image + mimes kiểm tra nội dung file thật, không tin đuôi file
'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048'],
]);
// Lưu vào storage/app/public/avatars với tên ngẫu nhiên, trả về đường dẫn.
$path = $request->file('avatar')->store('avatars', 'public');
$request->user()->update(['avatar' => $path]);
return back()->with('status', 'Đã cập nhật ảnh đại diện');
}
Các hàm hay dùng trên UploadedFile:
<?php
$file = $request->file('avatar');
$request->hasFile('avatar'); // có file được gửi lên
$file->isValid(); // upload thành công
$file->getClientOriginalName(); // tên gốc từ máy người dùng (KHÔNG tin)
$file->extension(); // đuôi đoán từ NỘI DUNG file (an toàn hơn)
$file->getMimeType(); // MIME thật
$file->getSize(); // byte
$file->store('avatars', 'public'); // tên ngẫu nhiên
$file->storeAs('avatars', "user-{$id}.webp", 'public'); // tự đặt tên
store() tự sinh tên file ngẫu nhiên nên không lo trùng tên hay tên file độc hại. Để ảnh trong storage/app/public truy cập được từ web, chạy một lần:
php artisan storage:link
Rồi lấy URL bằng Storage::url($path) hoặc asset('storage/' . $path).
Nhiều file (<input type="file" name="photos[]" multiple>):
<?php
$request->validate([
'photos' => ['array', 'max:10'],
'photos.*' => ['image', 'max:2048'],
]);
foreach ($request->file('photos', []) as $photo) {
$photo->store('photos', 'public');
}
Mỗi phần tử trong mảng đã là một object UploadedFile, dùng y như khi upload một file.
7. Thông tin request
<?php
$request->method(); // "GET", "POST"...
$request->isMethod('post'); // true/false, không phân biệt hoa thường
$request->path(); // "posts/5"
$request->url(); // "https://example.com/posts/5"
$request->fullUrl(); // kèm query string
$request->fullUrlWithQuery(['page' => 2]); // giữ query hiện tại, đổi page
$request->is('admin/*'); // so khớp path
$request->routeIs('posts.*'); // so khớp tên route
$request->route('post'); // tham số route {post}
$request->ip(); // IP client
$request->userAgent(); // trình duyệt
Header và token
<?php
$request->header('X-Request-Id'); // đọc header
$request->hasHeader('X-Request-Id');
$request->bearerToken(); // lấy "abc" từ "Authorization: Bearer abc"
Request muốn nhận gì?
<?php
$request->isJson(); // body gửi lên là JSON
$request->expectsJson(); // client muốn nhận JSON (AJAX hoặc Accept: application/json)
$request->ajax(); // có header X-Requested-With: XMLHttpRequest
expectsJson() hữu ích khi một action phục vụ cả form thường lẫn API:
<?php
return $request->expectsJson()
? response()->json($post)
: redirect()->route('posts.show', $post);
Về $request->ip() khi chạy sau proxy/CDN
Sau Cloudflare hoặc load balancer, ip() sẽ trả về IP của proxy. Laravel chỉ đọc X-Forwarded-For từ các proxy bạn khai báo tin cậy, trong bootstrap/app.php (Laravel 11+):
<?php
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*'); // hoặc danh sách IP proxy cụ thể
})
Header X-Forwarded-For ai cũng gửi được, nên chỉ tin nó khi website thật sự chạy sau proxy/CDN do bạn kiểm soát.
8. Người dùng đăng nhập
<?php
$user = $request->user(); // User hoặc null nếu chưa đăng nhập
$id = $request->user()?->id;
Tương đương auth()->user(). Với API dùng Sanctum: $request->user('sanctum'), hoặc đặt route trong middleware auth:sanctum rồi gọi $request->user() như bình thường.
9. Session: $request->session()
Laravel tự lưu phiên (file, database, Redis... theo SESSION_DRIVER trong .env) và tự quản lý cookie phiên đã mã hóa. Các thao tác hay dùng:
<?php
$request->session()->put('cart', $items); // ghi
$cart = $request->session()->get('cart', []); // đọc, có mặc định
$cart = $request->session()->pull('cart'); // đọc rồi xóa
$request->session()->forget('cart'); // xóa
$request->session()->has('cart');
// Flash: chỉ tồn tại cho request KẾ TIẾP
$request->session()->flash('status', 'Đã lưu');
// thường viết gọn hơn:
return redirect()->route('posts.index')->with('status', 'Đã lưu');
Helper session() cho kết quả tương tự: session('cart'), session(['cart' => $items]).
Đăng nhập và đăng xuất
Sau khi đăng nhập phải đổi mã phiên (chống session fixation), và khi đăng xuất phải hủy phiên + đổi CSRF token:
<?php
use Illuminate\Support\Facades\Auth;
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
return back()->withErrors(['email' => 'Sai email hoặc mật khẩu'])->onlyInput('email');
}
$request->session()->regenerate(); // chống session fixation
return redirect()->intended('/');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate(); // xóa dữ liệu phiên + đổi mã phiên
$request->session()->regenerateToken(); // đổi CSRF token
return redirect('/');
}
CSRF
Laravel tự sinh và kiểm tra token CSRF cho mọi request POST/PUT/PATCH/DELETE. Bạn chỉ cần thêm một dòng vào form:
<form method="post" action="{{ route('posts.store') }}">
@csrf
...
</form>
Thiếu @csrf thì request POST bị từ chối với lỗi 419 Page Expired.
10. Cookie
<?php
// Đọc: Laravel đã giải mã sẵn.
$theme = $request->cookie('theme', 'light');
// Ghi: gắn vào response (thời gian tính bằng PHÚT, không phải giây).
return response('OK')->cookie('theme', 'dark', 60 * 24 * 30);
// Hoặc xếp hàng để gắn vào response bất kỳ:
use Illuminate\Support\Facades\Cookie;
Cookie::queue('theme', 'dark', 60 * 24 * 30);
Cookie::queue(Cookie::forget('theme')); // xóa
Mặc định Laravel mã hóa mọi cookie nó tạo ra, và $request->cookie('theme') trả về giá trị đã giải mã ("dark"). Người dùng không sửa được giá trị cookie mà không làm hỏng nó.
11. Giữ lại dữ liệu form (old())
Khi validate lỗi, Laravel tự flash input cũ. Trong Blade:
<input name="title" value="{{ old('title') }}">
<input name="title" value="{{ old('title', $post->title) }}"> {{-- form sửa: mặc định là giá trị hiện tại --}}
@error('title')
<p class="error">{{ $message }}</p>
@enderror
Khi bạn tự trả về trang trước (không qua validate):
<?php
return back()->withInput()->withErrors(['title' => 'Tiêu đề đã tồn tại']);
// Không flash mật khẩu ngược lại form:
return back()->withInput($request->except('password'));
Lưu ý {{ }} trong Blade đã tự escape HTML (chống XSS), nên in dữ liệu người dùng nhập ra là an toàn. Chỉ cẩn thận với {!! !!} (in ra không escape).
12. Sửa dữ liệu request (trước khi xử lý)
<?php
$request->merge(['slug' => Str::slug($request->input('title'))]); // thêm/ghi đè
$request->mergeIfMissing(['status' => 'draft']); // chỉ thêm khi chưa có
Hữu ích trong middleware hoặc prepareForValidation() của Form Request. Không nên lạm dụng trong controller: thay vì sửa request, hãy chuẩn bị mảng dữ liệu riêng rồi truyền vào model.
13. Ví dụ tổng hợp: lọc, tìm kiếm và phân trang
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$q = $request->string('q')->trim()->toString();
$perPage = max(1, min($request->integer('per_page', 10), 50)); // chặn trong 1..50, tránh ?per_page=100000 hoặc -5
$posts = Post::query()
->with('user')
->when($q !== '', fn ($query) => $query->where('title', 'like', "%{$q}%"))
->when($request->boolean('mine'), fn ($query) => $query->whereBelongsTo($request->user()))
->latest()
->paginate($perPage)
->withQueryString(); // link trang 2, 3... giữ nguyên ?q=&mine=
return $request->expectsJson()
? response()->json($posts)
: view('posts.index', compact('posts', 'q'));
}
}
Chỉ một hàm, nhưng đã có ép kiểu an toàn, chặn giá trị quá lớn, tìm kiếm có điều kiện, phân trang giữ query string và phục vụ cả HTML lẫn JSON.
14. Bảng tra nhanh
| Muốn làm gì | Cách viết |
|---|---|
| Lấy một giá trị (URL, form, JSON) | $request->input('title') |
| Chỉ lấy từ query string | $request->query('q', '') |
| Lấy số nguyên | $request->integer('page', 1) (vẫn ra 0 nếu ?page=abc) |
| Lấy giá trị checkbox | $request->boolean('active') |
| Kiểm tra có giá trị không rỗng | $request->filled('x') |
| Validate | $request->validate([...]) / Form Request |
| Lấy dữ liệu đã validate | $request->validated() |
| Lưu file upload | $request->file('avatar')->store('avatars', 'public') |
| Method HTTP | $request->method() / $request->isMethod('post') |
| Đường dẫn hiện tại | $request->path() / $request->fullUrl() |
| Token API | $request->bearerToken() |
| IP người dùng | $request->ip() |
| Đọc cookie | $request->cookie('theme') |
| Đọc session | $request->session()->get('cart') |
| Đổi mã phiên sau đăng nhập | $request->session()->regenerate() |
| Người dùng đang đăng nhập | $request->user() |
| Chống CSRF trong form | @csrf |
15. Những lỗi hay gặp
$request->input('x')trả vềnulldù ô có giá trị? Kiểm tranamecủa input. Với JSON, kiểm tra headerContent-Type: application/json.- Ô để trống thành
nulllàm lỗi DB? DoConvertEmptyStringsToNull. Cách đúng là thêmnullablevào rule và cho cột nhậnNULL. Nếu thật sự cần chuỗi rỗng, dùng$request->string('x')->toString(). Đừng dùng$request->input('x', ''): giá trị mặc định chỉ áp dụng khi key không có mặt, còn ô để trống thì key vẫn có mặt với giá trịnull. $request->file()lànull? Form thiếuenctype="multipart/form-data", hoặc file vượtupload_max_filesize/post_max_sizetrongphp.ini(Laravel vẫn chạy trên PHP, các giới hạn này vẫn áp dụng).- Lỗi 419 Page Expired? Thiếu
@csrf, hoặc phiên đã hết hạn. session('x')trả vềnulldù đã ghi? Kiểm tra route có nằm trong middleware groupwebkhông. Route trongroutes/api.phpmặc định không có session.- Gọi
request()trong queue job hay command ra rỗng? Job và lệnh artisan không chạy trong một HTTP request. Lấy dữ liệu cần thiết từ request rồi truyền vào job, đừng đọc request bên trong job.
Kết luận
$request gom mọi dữ liệu người dùng gửi lên vào một chỗ, kèm sẵn ép kiểu, validate, upload, session và cookie đã mã hóa. Dùng đúng các hàm của nó giúp code ngắn hơn, dễ đọc hơn và ít lỗi bảo mật hơn.
Ba thói quen đáng giữ nhất:
- Luôn validate, và chỉ lưu
$request->validated(), không bao giờ$request->all(). - Dùng hàm ép kiểu (
integer,boolean,date...) thay vì đọc chuỗi rồi tự ép. - Dùng API của Laravel (
session(),cookie()) cho session và cookie.