Đang tải...

Cập nhật Avatar chuẩn Enterprise: Nghệ thuật quản lý file và dọn "rác" ổ cứng

05/05/2026
10 phút đọc
Cập nhật Avatar chuẩn Enterprise: Nghệ thuật quản lý file và dọn "rác" ổ cứng
Khi làm tính năng Cập nhật Avatar (Tải ảnh lên server), các bạn Junior thường làm theo bản năng: Nhận file -> quăng thẳng vào thư mục `public/uploads` -> lưu cái tên ...

Khi làm tính năng Cập nhật Avatar (Tải ảnh lên server), các bạn Junior thường làm theo bản năng: Nhận file -> quăng thẳng vào thư mục public/uploads -> lưu cái tên file vào Database.

Nhưng ở các hệ thống Enterprise, cách làm đó là một "tội ác" vì:

  1. Rác ổ cứng (Storage Bloat): Khi user đổi avatar mới, cái avatar cũ vẫn nằm chình ình trên server. 1 triệu user đổi avatar 10 lần, server của bạn sẽ tràn ổ cứng và sập.
  2. Lỗi HTTP Method: Rất nhiều anh em dính lỗi gửi form multipart/form-data qua phương thức PUT trên Postman và không nhận được file (do đặc thù của PHP).
  3. Vendor Lock-in: Code cứng đường dẫn lưu file vào hệ thống local. Khi dự án to lên, sếp bảo chuyển toàn bộ ảnh lên Amazon S3, bạn sẽ phải đập đi viết lại toàn bộ.

Hôm nay, anh em mình sẽ xây dựng một luồng Upload Avatar hoàn toàn mới, giải quyết triệt để 3 vấn đề trên bằng Storage Facade, Service PatternGarbage Collection (Dọn rác) nhé !!

Bạn hì hục viết một API Update Avatar bằng phương thức PUT. Bạn dùng Postman đính kèm file ảnh gửi lên. Laravel trả về lỗi mimes validation failed hoặc báo file không tồn tại. Bạn ngồi debug nửa ngày không hiểu tại sao.

Sự thật là: PHP mặc định KHÔNG hỗ trợ parse dữ liệu multipart/form-data thông qua các HTTP Method như PUT hay PATCH. Nếu muốn gửi file, bạn BẮT BUỘC phải dùng POST (hoặc dùng POST kèm thẻ spoofing _method=PUT).

Bài viết này sẽ hướng dẫn anh em thiết kế API chuẩn chỉ từ khâu hứng file, dọn dẹp file cũ, cho đến cách test Postman chuẩn xác nhất.

Bước 1: Khởi tạo dự án và Chuẩn bị Database

Tạo một project hoàn toàn mới:

laravel new avatar-upload-demo
cd avatar-upload-demo

1. Thêm cột avatar vào bảng Users

Mặc định bảng users của Laravel chưa có cột lưu ảnh đại diện. Ta sẽ tạo một migration để thêm vào.

php artisan make:migration add_avatar_to_users_table --table=users

Mở file migration vừa tạo lên:

public function up(): void
{
 Schema::table('users', function (Blueprint $table) {
 // Thêm cột avatar, mặc định là null
 $table->string('avatar')->nullable()->after('email');
 });
}

public function down(): void
{
 Schema::table('users', function (Blueprint $table) {
 $table->dropColumn('avatar');
 });
}

Chạy php artisan migrate để cập nhật Database.

2. Link Storage (Cực kỳ quan trọng)

Để các file lưu trong thư mục storage/app/public có thể truy cập được từ bên ngoài Internet, bạn phải tạo một symlink:

php artisan storage:link

Bước 2: "Khiên chắn" Form Request - Chặn đứng mã độc

Tuyệt đối không bao giờ tin tưởng file do user đẩy lên. Hacker có thể đổi đuôi file .php thành .jpg để bypass nếu bạn không check kỹ.

php artisan make:request UpdateAvatarRequest
// app/Http/Requests/UpdateAvatarRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateAvatarRequest extends FormRequest
{
 public function authorize(): bool
 {
 return true; 
 }

 public function rules(): array
 {
 return [
 // Bắt buộc phải là file ảnh, giới hạn định dạng an toàn, dung lượng tối đa 2MB (2048 KB)
 'avatar' => 'required|image|mimes:jpeg,png,jpg,webp|max:2048',
 ];
 }

 public function messages(): array
 {
 return [
 'avatar.required' => 'Vui lòng chọn một ảnh.',
 'avatar.image' => 'File tải lên phải là định dạng ảnh.',
 'avatar.mimes' => 'Ảnh chỉ hỗ trợ đuôi jpeg, png, jpg, webp.',
 'avatar.max' => 'Dung lượng ảnh không được vượt quá 2MB.',
 ];
 }
}

Bước 3: AvatarService - Cỗ máy xử lý và "Dọn rác"

Đây là tinh hoa của bài viết. Chúng ta sẽ dùng Illuminate\Support\Facades\Storage. Việc dùng Storage giúp bạn lưu file vào ổ cứng máy chủ (Local) hôm nay, nhưng ngày mai có thể chuyển sang Amazon S3 cực kỳ dễ dàng chỉ bằng cách đổi config trong .env.

mkdir app/Services

Tạo file app/Services/AvatarService.php:

// app/Services/AvatarService.php
namespace App\Services;

use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;

class AvatarService
{
 /**
 * @param User $user User cần đổi avatar
 * @param UploadedFile $file File ảnh từ request
 * @return string Path của ảnh mới
 */
 public function updateAvatar(User $user, UploadedFile $file): string
 {
 try {
 // 1. Kiểm tra và Xóa avatar CŨ để giải phóng ổ cứng
 if ($user->avatar && Storage::disk('public')->exists($user->avatar)) {
 Storage::disk('public')->delete($user->avatar);
 Log::info("Đã xóa avatar cũ của user {$user->id}");
 }

 // 2. Tạo tên file độc nhất (chống trùng lặp tên)
 $filename = 'user_' . $user->id . '_' . time() . '.' . $file->getClientOriginalExtension();

 // 3. Lưu file MỚI vào thư mục storage/app/public/avatars
 // Sử dụng putFileAs để vừa lưu vừa đổi tên
 $path = Storage::disk('public')->putFileAs('avatars', $file, $filename);

 // 4. Lưu đường dẫn vào Database
 $user->update(['avatar' => $path]);

 return $path;

 } catch (\Exception $e) {
 Log::error("Lỗi khi update avatar: " . $e->getMessage());
 throw new \Exception("Không thể cập nhật ảnh đại diện lúc này.");
 }
 }
}

Bước 4: Controller siêu mỏng và Route

php artisan make:controller Api/AvatarController
// app/Http/Controllers/Api/AvatarController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\UpdateAvatarRequest;
use App\Services\AvatarService;
use Illuminate\Support\Facades\Storage;

class AvatarController extends Controller
{
 protected AvatarService $avatarService;

 public function __construct(AvatarService $avatarService)
 {
 $this->avatarService = $avatarService;
 }

 public function update(UpdateAvatarRequest $request)
 {
 /** @var \App\Models\User $user */
 $user = $request->user(); // Lấy user đang đăng nhập

 // Chuyển file sang Service xử lý
 $path = $this->avatarService->updateAvatar($user, $request->file('avatar'));

 // Trả về full URL để Frontend có thể hiển thị ngay lập tức
 return response()->json([
 'success' => true,
 'message' => 'Cập nhật ảnh đại diện thành công.',
 'data' => [
 'avatar_url' => asset('storage/' . $path) // VD: http://domain.com/storage/avatars/user_1_123.jpg
 ]
 ]);
 }
}

Mở routes/api.php đăng ký Route: (Chú ý: Ta dùng POST thay vì PUT để xử lý multipart/form-data mượt mà nhất trong PHP).

use App\Http\Controllers\Api\AvatarController;

Route::middleware('auth:sanctum')->group(function () {
 // Dùng POST để upload file
 Route::post('/user/avatar', [AvatarController::class, 'update']); 
});

Bước 5: Thử lửa với Postman từ A-Z

Để test API này, bạn cần có 1 User trong DB và đã đăng nhập (có Token).

Bước 5.1: Tạo User nhanh qua Tinker (Bypass Authentication setup) Mở Terminal gõ:

php artisan tinker

Trong cửa sổ Tinker, gõ các lệnh sau để tạo 1 User và sinh luôn 1 Token test:

$user = User::factory()->create();
$token = $user->createToken('TestApp')->plainTextToken;
echo $token; 
// Nó sẽ in ra token, ví dụ: 1|LaravelSanctumTokenXYZ... Hãy copy nó!

Bước 5.2: Test Upload trên Postman Khởi động server: php artisan serve.

  1. Method: POST
  2. URL: [http://127.0.0.1:8000/api/user/avatar](http://127.0.0.1:8000/api/user/avatar)
  3. Headers:
  • Accept: application/json
  • Authorization: Bearer 1|LaravelSanctumTokenXYZ... (Dán token vừa copy ở trên vào)

4. Body:

  • Chọn tab form-data (Cực kỳ quan trọng, không dùng raw hay x-www-form-urlencoded).
  • Cột Key: Gõ chữ avatar. Xong rê chuột vào rìa bên phải của chữ avatar, nó hiện ra cái dropdown, chọn kiểu là File thay vì Text.
  • Cột Value: Bấm vào nút Select Files và chọn 1 bức ảnh dưới 2MB từ máy tính của bạn.

Nhấn SEND và xem kết quả:

{
 "success": true,
 "message": "Cập nhật ảnh đại diện thành công.",
 "data": {
 "avatar_url": "http://127.0.0.1:8000/storage/avatars/user_1_1714890000.jpg"
 }
}

Bước 5.3: Cảm nhận sự "Sạch sẽ" của kiến trúc

  1. Hãy click vào cái link avatar_url trong response trên trình duyệt, bạn sẽ thấy ảnh của mình hiện ra (Nhờ lệnh storage:link ở Bước 1).
  2. Test dọn rác: Hãy mở Postman lên, chọn một bức ảnh khác và nhấn SEND lần nữa. Sau đó, bạn mở thư mục storage/app/public/avatars trong source code ra xem. Bạn sẽ thấy CHỈ CÓ DUY NHẤT 1 BỨC ẢNH MỚI NHẤT. Bức ảnh cũ đã bị AvatarService chém bay màu. Ổ cứng của bạn đã được cứu!

Tóm lại

Làm một API upload file không hề khó. Cái khó là làm sao để:

  1. An toàn: Validation chặt chẽ qua FormRequest.
  2. Dễ bảo trì: Dùng Storage Facade để trừu tượng hóa ổ cứng (Sẵn sàng bay lên S3 bất cứ lúc nào).
  3. Có trách nhiệm: Tự dọn dẹp rác (Garbage Collection) khi thay thế file cũ.
  4. Hiểu đặc thù ngôn ngữ: Nắm được điểm mù của PHP khi xử lý multipart/form-data qua các phương thức RESTful.

Chúc anh em áp dụng thành công kiến trúc này để hệ thống luôn gọn nhẹ và sạch sẽ nhé!

📚 Nguồn: Viblo

Chia sẻ bài viết

Cần tư vấn?

Liên hệ với chúng tôi để được hỗ trợ

Liên hệ ngay

Bài viết liên quan

Bài 1 — Redis là gì và vì sao Backend Developer nên quan tâm?
05/05/2026

Bài 1 — Redis là gì và vì sao Backend Developer nên quan tâm?

# Redis giải quyết bài toán gì? ![](https://images.viblo.asia/fad2cce6-2455-4bc6-bbc9-b19f7f0d5309.png) Khi làm backend một "thời gian" (thực ra là mới 2 năm kinh nghiệm=))), bạn...

Đọc thêm
EN-FAB Inc. | Enhanced Oil Recovery & Metering Solutions | USA
05/05/2026

EN-FAB Inc. | Enhanced Oil Recovery & Metering Solutions | USA

**[EN-FAB Inc. | Enhanced Oil Recovery & Metering Solutions | USA](https://www.en-fabinc.com/)** EN-FAB Inc. delivers Enhanced Oil Recovery, Metering & Measurement, and Gas Gathering solutions in USA....

Đọc thêm
Android Studio Panda 4: Code không chỉ nhanh mà còn “biết nghĩ trước”
05/05/2026

Android Studio Panda 4: Code không chỉ nhanh mà còn “biết nghĩ trước”

Android Studio Panda 4 hiện đã ở phiên bản ổn định (stable) và sẵn sàng để bạn sử dụng cho các dự án thực tế. Bản phát hành này mang đến **Planning Mode (Ch?...

Đọc thêm

Bắt đầu dự án của bạn

Hãy để Flash Dev đồng hành cùng bạn

Liên hệ ngay