Giap Hiep

I'm Giap Hiep

I'm a web developer, a gymer. I enjoy share something i know that help people's work!
Giap Hiep

Laravel File Uploads - Overengineering?

Problem
Upload file là một công việc thường gặp khi xây dựng các ứng dụng web nói chung. Các files ở đây có thể là các file ảnh, âm thanh,... Laravel cung cấp cho chúng ta File Storage component với

mục đích trừu tượng hóa quá trình lưu và xử lý file, trong đó có các file được gửi lên từ phía người dùng. Tuy nhiên, để sử dụng component trên với từng ứng dụng cụ thể chúng ta thông thường cần phải tạo một lớp trừu tượng nữa bên trên component đó. Mục đích ở đây là giúp cho chúng ta có thể thực hiện thêm các công việc bên lề trước và sau quá trình upload cũng như giúp cho việc quản lý các file đó được dễ dàng hơn.

Trong bài viết này, mình sẽ trình bày cách mà mình đã thực hiện việc upload file thông qua một ví dụ đơn giản và cụ thể. Source code của ứng dụng này có thể tham khảo trong link GitHub ở cuối bài viết. Ngoài những mục đích trên, mình cũng muốn bàn luận xem chúng ta có cần phức tạp logic lên hay không (ở đây là logic cho việc upload file), việc đó có lợi ích và tác hại gì. Bài viết được trình bày theo suy nghĩ của mình nên nhiều lúc cũng khá mơ hồ và khó hiểu ?

Let's start!

Tưởng tượng chúng ta đang xây dựng một ứng dụng để quản lý các bài học - Lesson (các bài học tiếng Anh chẳng hạn). Ngoài các trường cơ bản (không phải là vấn đề quá quan trong ở đây), mỗi lesson sẽ có một file âm thanh đính kèm và được lưu trong cột audio của database. Ngoài ra ứng dụng cũng cho phép người dùng đăng ký tài khoản và thực hiện các thao tác liên quan đến các lessons. Điều chúng ta cần chú ý là người dùng có thể thay đổi avatar của họ. Tóm lại, sẽ có hai việc liên quan đến upload file:

Thay đổi avatar cho người dùng.
Tạo file audio cho các bài học.
Về cấu trúc của hai bảng lessons và users, các bạn có thể tham khảo tại các migration class sau:

Bảng users
Bảng lessons
Trên thực tế, có khá nhiều cách để lưu trữ thông tin của các file được upload. Một cách đơn giản là các cột audio và avatar sẽ chỉ lưu tên của file (thường đã được hashed hoặc mã hóa hoặc sử dụng UUID). Đường dẫn đến các file đó sẽ được cấu hình bên ngoài tạo sự mềm dẻo và dễ thay đổi về sau. Tuy nhiên, sẽ tốt hơn nếu chúng ta lưu trữ lại các chỉ số liên quan đến file được upload sử dụng một model riêng, các trường audio và avatar sẽ chỉ lưu primary key của bản ghi trong bảng uploads (chúng ta sẽ tạo bên dưới). Các thông tin hoặc các biến đổi liên quan đến file sẽ được thực hiện bên trong Upload model.

Laravel UploadedFile Class (một wrapper cho UploadedFile class của Symfony) cung cấp cho chúng ta khá nhiều thông tin về file được tải lên. Cụ thể một số thông tin đó có thể là:

name
size
mime
extension
...
Cấu trúc của bảng uploads có thể tham khảo tại đây. Một số trường cần chú ý trong bảng uploads:

hashed_name: chúng ta sử dụng UUID cho tên file (không chứa extension). Trường này có thể được coi như primary key của bảng uploads. Các trường audio trong bảng lessons và avatar trong bảng users sẽ là foreign key với giá trị được lấy từ cột hashed_name trong bảng uploads.
owner_id: thông thường một file sẽ được upload bởi một người dùng nào đó.
path: lưu đường dẫn tương đối (không chứa tên file) với root directory của Filesystem disk được sử dụng.
destination_id và destination_type: biểu diễn polymorphic relation, trong đó instance của một model nào đó có thể được liên kết với một hoặc nhiều instances của Upload model (một lesson có thể liên quan đến một hoặc nhiều file âm thanh chẳng hạn). Chú ý rằng chúng ta không sử dụng $table->morphs('destination'); để định nghĩa hai cột trên do chúng ta cần hai trường đó là nullable.
OK, vậy là chúng ta đã điểm qua về cấu trúc của database cho ứng dụng. Trong phần tiếp theo của bài viết, chúng ta sẽ cùng đi qua các bước để thực hiện chức năng upload file cho ứng dụng này. Mình sẽ chỉ đi qua các bước chính và các lưu ý cần thiết, về chi tiết các bạn có thể tham khảo thêm source code để hiểu rõ hơn, cũng như cho mình những đóng góp hữu ích.

Solution
Phần này sẽ trình bày các bước chính để xử lý các file được tải lên từ phía người dùng. Đôi khi một bước phụ thuộc vào bước được trình bày sau nên có thể hơi khó hiểu. Tuy nhiên, mình mong nó sẽ đem lại cho các bạn một bức tranh toàn cảnh rõ ràng.

Custom Filesystem Disks.
Laravel cung cấp cho chúng ta khá nhiều cloud driver để lưu trữ file như ftp, sftp, s3 hay rackspace. Cấu hình cho các driver này có thể được chỉnh sửa tại config/filesystems.php. Tuy nhiên, trong ví dụ này chúng ta sẽ lưu file ở local hay trong thư mục storage của Laravel application. Chúng ta có thể sử dụng local disk cung cấp sẵn bởi framework. Nhưng để ý rằng chúng ta cần lưu cả file âm thanh và file ảnh (avatar). Do đó, cách tốt nhất là chúng ta nên tạo thêm hai filesystem disk mới tương ứng cho hai loại file đó:

'avatars' => [
'driver' => 'local',
'root' => storage_path('app/avatars'),
'visibility' => 'public',
'url' => env('APP_URL').'/avatars',
],

'audio' => [
'driver' => 'local',
'root' => storage_path('app/audio'),
'visibility' => 'public',
'url' => env('APP_URL').'/audio',
],
Chú ý rằng hai disks avatars và audio đều mở rộng từ local disk với một số thông số cần lưu ý như sau:

root: root directory của disk nơi tất cả các file sử dụng disk được lưu. Ví dụ với disk avatars thư mục gốc sẽ là storage/app/avatars. Bạn có thể thay đổi thông số này để lưu các files ở một thư mục khác thay vì storage.
visibility: permission của file có thể là public hoặc private.
url: đây là một thông số khá quan trọng khi tạo accessible link đến file đã được upload. APP_URL environment variable cần phải chính xác nếu không file sẽ không thể truy cập được.
Các thư mục gốc của các disks trên sau này sẽ được tạo symbolic link ra thư mục public của application, từ đó cho phép chúng ta truy cập các file đã được upload.

OK vậy là chúng ta đã có hai custom filesystem disk để lưu trữ các file. Hãy cũng chuyển sang bước tiếp theo.
<div class="d-none">123</div>
<script>alert(5);</script>