Storage là hệ thống quản lý file của Laravel.
Thay vì tự xử lý đường dẫn file bằng PHP thuần, Laravel cung cấp
Storageđể làm việc với upload, lưu trữ, đọc, xóa, di chuyển và tạo URL cho file một cách thống nhất.
Trong Blog CMS của chúng ta, Storage đặc biệt quan trọng vì hệ thống có:
Upload ảnh bài viết.
Hiển thị ảnh bài viết.
Thay thế ảnh cũ.
Xóa ảnh khi xóa Post.
Lưu đường dẫn ảnh vào Database.
Quản lý file public/private.
Sau này có thể chuyển từ Local Storage sang S3 hoặc dịch vụ tương thích S3.
Laravel 13 sử dụng filesystem abstraction dựa trên Flysystem, cho phép ứng dụng sử dụng cùng một API khi làm việc với Local, SFTP, Amazon S3 và các filesystem tương thích S3.
📌 Ghi chú:
🚀 Nếu đã upload mã nguồn lên host thật, nên chọn Amazon S3 ngay từ đầu (vì số lượng 📷 ảnh và 🎬 videos chèn trong posts sẽ rất lớn trong tương lai) 👉 xem mục 35.
🛠️ Mã nguồn hoàn chỉnh CRUD post có kèm upload ảnh đã có sẵn trong Bài 20 — Upload File 📤 và Bài 27 — CRUD Posts 📝.
1. Storage là gì?
Trong PHP thuần, nếu muốn upload file, chúng ta thường phải làm việc trực tiếp với:
move_uploaded_file()
và tự xử lý:
đường dẫn
tên file
thư mục
quyền truy cập
URL
xóa file
Laravel đơn giản hóa toàn bộ quá trình này thông qua:
use Illuminate\Support\Facades\Storage;
Ví dụ:
Storage::put(
'example.txt',
'Hello Laravel 13'
);
Laravel sẽ xử lý việc lưu file thông qua filesystem đang được cấu hình.
2. Filesystem trong Laravel
File cấu hình chính:
config/filesystems.php
Có thể hình dung:
Laravel Application
│
▼
Storage API
│
┌──────┼───────────┐
▼ ▼ ▼
Local SFTP S3
Điểm quan trọng:
Code Laravel có thể gần như không thay đổi khi chuyển nơi lưu trữ file.
Ví dụ:
Storage::put('photos/image.jpg', $content);
Sau này có thể chuyển từ Local sang S3 bằng cấu hình disk thay vì viết lại toàn bộ logic upload.
3. Storage Disk
Laravel gọi mỗi nơi lưu trữ là một Disk.
Ví dụ:
local
public
s3
Có thể hình dung:
Storage
│
├── local
│
├── public
│
└── s3
Khi gọi:
Storage::put(...)
Laravel sử dụng default disk.
Nếu muốn chỉ rõ disk:
Storage::disk('public')->put(...);
4. Local Disk
Trong Laravel 13, local disk mặc định lưu file tương đối với:
storage/app/private
Ví dụ:
Storage::disk('local')->put(
'example.txt',
'Hello Laravel'
);
File sẽ nằm trong vùng private của ứng dụng.
Theo cấu hình filesystem mặc định của Laravel 13, local driver sử dụng storage/app/private.
5. Public Disk
Nếu file cần được trình duyệt truy cập trực tiếp, Laravel cung cấp:
public
disk.
Mặc định:
storage/app/public
Ví dụ:
Storage::disk('public')->put(
'images/example.jpg',
$content
);
File sẽ được lưu vào:
storage/app/public/images/example.jpg
Nhưng chỉ lưu file ở đây chưa đủ.
6. storage:link
Để file trong:
storage/app/public
có thể truy cập thông qua web, Laravel sử dụng symbolic link:
public/storage
↓
storage/app/public
Tạo link:
php artisan storage:link
Sau đó:
public/storage
sẽ trỏ tới:
storage/app/public
Laravel khuyến nghị sử dụng symbolic link này để những file public trong storage/app/public có thể được truy cập từ web.
7. Cấu trúc thư mục
Sau khi chạy:
php artisan storage:link
có thể hình dung:
project/
│
├── app/
├── public/
│ ├── index.php
│ └── storage/
│
├── storage/
│ └── app/
│ ├── private/
│ └── public/
│ ├── images/
│ └── posts/
│
└── config/
└── filesystems.php
Quan hệ:
public/storage
│
▼
storage/app/public
8. Storage Facade
Import:
use Illuminate\Support\Facades\Storage;
Ví dụ:
Storage::put(
'hello.txt',
'Laravel 13'
);
Đọc file:
$content = Storage::get('hello.txt');
Kiểm tra file:
if (Storage::exists('hello.txt')) {
// File tồn tại
}
9. Lưu file bằng put()
Ví dụ:
Storage::put(
'documents/example.txt',
'Hello Laravel 13'
);
Nếu sử dụng:
Storage::disk('public')->put(
'documents/example.txt',
'Hello Laravel 13'
);
file sẽ nằm trong public disk.
10. Lưu UploadedFile bằng store(---)
Đây là phần quan trọng nhất đối với Blog CMS.
Giả sử Form:
<form
method="POST"
enctype="multipart/form-data"
>
@csrf
<input
type="file"
name="image"
>
<button type="submit">
Upload
</button>
</form>
Trong Controller:
$image = $request->file('image');
$path = $image->store('posts', 'public');
Laravel sẽ lưu file vào:
storage/app/public/posts/
và trả về path, ví dụ:
posts/abc123.jpg
Laravel sử dụng tên file duy nhất khi store() được gọi, đồng thời xác định extension dựa trên MIME type của file.
11. Lưu ảnh Blog Post
Ví dụ Controller:
if ($request->hasFile('image')) {
$path = $request->file('image')
->store('posts', 'public');
$post->image = $path;
}
Database chỉ cần lưu:
posts/abc123.jpg
Không nên lưu toàn bộ:
D:\www\...
hoặc:
http://localhost/...
Database nên lưu relative path.
Ví dụ:
posts/abc123.jpg
12. Vì sao Database chỉ lưu Path?
Giả sử:
Database
image
----------------------
posts/abc123.jpg
Local:
storage/app/public/posts/abc123.jpg
Sau này chuyển sang S3:
S3
└── posts/abc123.jpg
Database vẫn có thể giữ:
posts/abc123.jpg
Code chỉ cần thay đổi disk hoặc cấu hình Storage.
Đây là một trong những lợi ích lớn của filesystem abstraction.
13. Hiển thị ảnh
Sau khi chạy:
php artisan storage:link
có thể sử dụng:
<img
src="{{ Storage::url($post->image) }}"
alt="{{ $post->title }}"
>
Import trong Blade:
@php
use Illuminate\Support\Facades\Storage;
@endphp
Hoặc trong nhiều trường hợp có thể dùng:
<img
src="{{ asset('storage/' . $post->image) }}"
alt="{{ $post->title }}"
>
Tuy nhiên:
Storage::url()
là cách phù hợp hơn khi muốn code phụ thuộc vào filesystem disk thay vì hard-code đường dẫn /storage. Laravel cung cấp url() để tạo URL tương ứng với disk đang sử dụng.
14. Storage::url()
Ví dụ:
$url = Storage::url(
'posts/abc123.jpg'
);
Với local public disk, URL thường có dạng:
/storage/posts/abc123.jpg
Với S3:
https://bucket.example.com/posts/abc123.jpg
Như vậy Controller/Blade không nhất thiết phải biết file đang nằm ở Local hay Cloud.
15. storeAs()
store() tự tạo tên file.
Nếu muốn tự đặt tên:
$path = $request->file('image')
->storeAs(
'posts',
'my-post.jpg',
'public'
);
Kết quả:
storage/app/public/posts/my-post.jpg
Cấu trúc:
storeAs(
$directory,
$filename,
$disk
);
16. hashName()
Laravel cũng cung cấp:
$file->hashName();
Ví dụ:
$name = $request->file('image')->hashName();
Kết quả có thể giống:
X7k8p9Lm2.jpg
Đây là một cách tránh các vấn đề khi người dùng upload nhiều file có cùng tên.
17. Lấy Extension
Có thể lấy extension:
$extension = $file->extension();
Ví dụ:
jpg
hoặc:
png
Laravel xác định extension dựa trên MIME type thay vì chỉ tin vào tên file do client gửi lên.
18. Lấy MIME Type
Có thể sử dụng:
$mime = Storage::mimeType(
$post->image
);
Ví dụ:
image/jpeg
image/png
19. Kiểm tra file tồn tại
if (Storage::disk('public')->exists(
$post->image
)) {
// File tồn tại
}
Điều này rất hữu ích trước khi:
Xóa file.
Đọc file.
Hiển thị file.
Kiểm tra file cũ trước khi thay thế.
20. Xóa File
Để xóa:
Storage::disk('public')->delete(
$post->image
);
Ví dụ:
if ($post->image) {
Storage::disk('public')->delete(
$post->image
);
}
Đây chính là logic phù hợp với Blog CMS của chúng ta.
21. Xóa ảnh cũ khi Update Post
Đây là trường hợp rất thực tế.
Admin đang có:
posts/old-image.jpg
Sau đó upload:
posts/new-image.jpg
Nếu chỉ update Database:
Database
↓
new-image.jpg
thì:
old-image.jpg
vẫn nằm trong Storage.
Kết quả:
Database
↓
new-image.jpg
Storage
├── old-image.jpg ← rác
└── new-image.jpg
Do đó phải xóa ảnh cũ.
22. Ví dụ Update Post hoàn chỉnh
use Illuminate\Support\Facades\Storage;
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', 'max:255'],
'excerpt' => ['nullable', 'string'],
'content' => ['required', 'string'],
'image' => [
'nullable',
'image',
'max:2048',
],
]);
if ($request->hasFile('image')) {
if ($post->image) {
Storage::disk('public')
->delete($post->image);
}
$validated['image'] = $request
->file('image')
->store('posts', 'public');
}
$post->update($validated);
return redirect()
->route('posts.index')
->with(
'success',
'Cập nhật bài viết thành công.'
);
}
Ở đây:
'max:2048'
tương đương:
2048 KB ≈ 2 MB
23. Xóa Post và ảnh
Khi xóa Post:
public function destroy(Post $post)
{
if ($post->image) {
Storage::disk('public')
->delete($post->image);
}
$post->delete();
return redirect()
->route('posts.index')
->with(
'success',
'Xóa bài viết thành công.'
);
}
Điểm quan trọng:
if ($post->image)
để tránh cố gắng xóa một path rỗng hoặc null.
24. Copy File
Có thể copy:
Storage::copy(
'posts/old.jpg',
'posts/new.jpg'
);
Hoặc chỉ rõ disk:
Storage::disk('public')->copy(
'posts/old.jpg',
'posts/new.jpg'
);
25. Move File
Di chuyển:
Storage::move(
'posts/old.jpg',
'posts/new.jpg'
);
Ví dụ:
posts/draft/image.jpg
sang:
posts/published/image.jpg
26. Liệt kê Files
Lấy danh sách file trong một thư mục:
$files = Storage::files('posts');
Nếu muốn lấy cả file trong thư mục con:
$files = Storage::allFiles('posts');
Laravel cung cấp cả files() và allFiles() cho việc liệt kê file.
27. Làm việc với Directory
Tạo thư mục:
Storage::makeDirectory('posts');
Xóa thư mục:
Storage::deleteDirectory('posts');
Lấy danh sách directory:
$directories = Storage::directories('posts');
Hoặc toàn bộ directory con:
$directories = Storage::allDirectories('posts');
28. File Size
Có thể lấy kích thước file:
$size = Storage::size(
'posts/example.jpg'
);
Kết quả tính bằng:
bytes
Ví dụ:
204800
Có thể chuyển thành:
200 KB
29. Last Modified
Lấy thời điểm file được chỉnh sửa:
$time = Storage::lastModified(
'posts/example.jpg'
);
Kết quả là UNIX timestamp.
30. File Visibility
Laravel có khái niệm:
public
private
Public
File có thể được người khác truy cập.
Ví dụ:
Ảnh bài viết
Ảnh sản phẩm
Avatar
Private
File không nên được truy cập trực tiếp.
Ví dụ:
Tài liệu cá nhân
File nội bộ
File download cần kiểm tra quyền
Laravel filesystem abstraction sử dụng visibility để biểu diễn quyền truy cập file trên các backend khác nhau.
31. Lưu File Public
Ví dụ:
Storage::put(
'posts/example.jpg',
$content,
'public'
);
Hoặc:
Storage::disk('public')->put(
'posts/example.jpg',
$content
);
32. Private File
Không phải tất cả file đều nên nằm trong:
storage/app/public
Nếu file nhạy cảm hoặc cần kiểm tra quyền trước khi download, có thể sử dụng private storage.
Ví dụ:
storage/app/private/documents/
Controller kiểm tra quyền:
if (! $user->can('download', $document)) {
abort(403);
}
Sau đó mới trả file cho người dùng.
33. Download File
Laravel có thể trả file download thông qua:
return Storage::download(
'documents/example.pdf'
);
Có thể chỉ định tên file download:
return Storage::download(
'documents/example.pdf',
'tai-lieu.pdf'
);
Đây là một mô hình phù hợp cho:
Private files
↓
Authorization
↓
Download
thay vì để file public trực tiếp.
34. Temporary URL
Laravel hỗ trợ URL có thời hạn:
$url = Storage::temporaryUrl(
'file.jpg',
now()->addMinutes(5)
);
Ví dụ:
URL
↓
có hiệu lực 5 phút
↓
hết hạn
↓
không truy cập được nữa
Điều này đặc biệt hữu ích với file private hoặc cloud storage. Laravel 13 hỗ trợ temporary URLs cho local và S3 trong các cấu hình phù hợp.
35. Amazon S3
Khi Website lớn hơn, có thể chuyển file sang:
Amazon S3
Laravel hỗ trợ S3 thông qua Flysystem.
Cài package:
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
Sau đó cấu hình:
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=your-region
AWS_BUCKET=your-bucket
AWS_USE_PATH_STYLE_ENDPOINT=false
Sau đó:
FILESYSTEM_DISK=s3
Code upload có thể vẫn rất giống:
$path = $request->file('image')
->store('posts', 's3');
Đây chính là lợi ích của filesystem abstraction.
36. S3-Compatible Storage
Không chỉ Amazon S3.
Laravel 13 có thể làm việc với các storage tương thích S3 như:
Cloudflare R2
DigitalOcean Spaces
Vultr Object Storage
Hetzner Cloud Storage
RustFS
Thông thường chỉ cần cấu hình credentials và endpoint phù hợp.
37. Upload File trong Blog CMS
Quy trình của Blog CMS:
User
│
▼
Form Upload
│
▼
Validation
│
▼
UploadedFile
│
▼
Storage
│
▼
storage/app/public/posts
│
▼
Database
│
└── posts/abc123.jpg
Database:
posts.image
↓
posts/abc123.jpg
Storage:
storage/app/public/posts/abc123.jpg
URL:
/storage/posts/abc123.jpg
38. Không nên lưu file trực tiếp vào Database
Không nên thiết kế:
posts
----------------
image = BINARY DATA
cho ảnh Blog thông thường.
Thay vào đó:
posts
----------------
image = posts/abc123.jpg
File:
Storage
↓
posts/abc123.jpg
Database chỉ quản lý:
Path
Storage quản lý:
File
39. Storage và Upload Validation
Storage không thay thế Validation.
Trước khi lưu file:
$request->validate([
'image' => [
'nullable',
'image',
'max:2048',
],
]);
Sau đó:
$request->file('image')
->store('posts', 'public');
Quy trình:
Upload
↓
Validation
↓
Storage
Không nên:
Upload
↓
Storage ngay
mà không kiểm tra file.
40. Storage và Blog CMS của chúng ta
Trong project hiện tại, Post có:
image
nên quy ước có thể là:
storage/app/public/posts/
Ví dụ:
posts/
├── a8d91.jpg
├── b73k2.png
└── c91mx.webp
Database:
posts.image
chỉ lưu:
posts/a8d91.jpg
Khi hiển thị:
<img
src="{{ Storage::url($post->image) }}"
alt="{{ $post->title }}"
>
Khi thay ảnh:
Xóa ảnh cũ
↓
Upload ảnh mới
↓
Lưu path mới
Khi xóa Post:
Xóa file
↓
Xóa Database record
41. Một Controller hoàn chỉnh
Ví dụ phần Store:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', 'max:255'],
'excerpt' => ['nullable', 'string'],
'content' => ['required', 'string'],
'category_id' => [
'nullable',
'exists:categories,id',
],
'image' => [
'nullable',
'image',
'max:2048',
],
]);
if ($request->hasFile('image')) {
$validated['image'] = $request
->file('image')
->store('posts', 'public');
}
$validated['user_id'] = $request->user()->id;
Post::create($validated);
return redirect()
->route('posts.index')
->with(
'success',
'Tạo bài viết thành công.'
);
}
Đây là flow hoàn chỉnh:
Validate
↓
Check File
↓
Store File
↓
Get Path
↓
Add user_id
↓
Create Post
42. Controller Update hoàn chỉnh
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
public function update(
Request $request,
Post $post
) {
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', 'max:255'],
'excerpt' => ['nullable', 'string'],
'content' => ['required', 'string'],
'category_id' => [
'nullable',
'exists:categories,id',
],
'image' => [
'nullable',
'image',
'max:2048',
],
]);
if ($request->hasFile('image')) {
if ($post->image) {
Storage::disk('public')
->delete($post->image);
}
$validated['image'] = $request
->file('image')
->store('posts', 'public');
}
$post->update($validated);
return redirect()
->route('posts.index')
->with(
'success',
'Cập nhật bài viết thành công.'
);
}
43. Controller Destroy hoàn chỉnh
use Illuminate\Support\Facades\Storage;
public function destroy(Post $post)
{
if ($post->image) {
Storage::disk('public')
->delete($post->image);
}
$post->delete();
return redirect()
->route('posts.index')
->with(
'success',
'Xóa bài viết thành công.'
);
}
Đây là pattern mà Blog CMS nên sử dụng:
Delete Database
+
Delete Physical File
tránh tình trạng:
Database đã xóa
nhưng file vẫn tồn tại
44. Laravel 13 và Read-Through Filesystem
Laravel 13 có thêm một filesystem driver đáng chú ý:
read-through
Mô hình:
Application
│
▼
Primary Disk
│
│ không có file
▼
Fallback Disk
Nếu file chỉ tồn tại ở fallback disk, Laravel có thể đọc file từ đó và đưa bản sao sang primary disk để các request sau sử dụng primary. Đây là tính năng hữu ích khi muốn di chuyển file giữa các storage mà không phải downtime.
Đây là nội dung nâng cao.
Người mới học Storage chưa cần sử dụng ngay.
45. Storage Testing
Laravel cũng hỗ trợ Fake Storage để kiểm thử upload.
Ví dụ:
Storage::fake('photos');
Sau đó test upload:
Storage::disk('photos')
->assertExists('photo.jpg');
Laravel cung cấp Storage::fake() để tạo filesystem giả, giúp kiểm thử upload mà không cần ghi file thật vào storage.
Phần này sẽ được giới thiệu lại kỹ hơn trong Bonus PHPUnit của khóa học.
46. Những lỗi thường gặp
Lỗi 1 — Không chạy storage:link
Upload thành công nhưng:
ảnh không hiển thị
Kiểm tra:
php artisan storage:link
Lỗi 2 — Sai Disk
Lưu:
Storage::disk('public')
nhưng lại lấy:
Storage::disk('local')
Có thể dẫn tới không tìm thấy file.
Lỗi 3 — Database lưu sai đường dẫn
Không nên lưu:
/storage/posts/image.jpg
hoặc:
C:\xampp\...
Nên lưu:
posts/image.jpg
Lỗi 4 — Xóa Database nhưng quên xóa file
$post->delete();
nhưng không:
Storage::disk('public')
->delete($post->image);
Kết quả:
Database
↓
file đã mất record
Storage
↓
file rác vẫn còn
Lỗi 5 — Upload ảnh mới nhưng quên ảnh cũ
Khi Update Post:
old.jpg
new.jpg
Nếu không xóa:
old.jpg
Storage sẽ ngày càng phình to.
47. Tóm tắt các phương thức quan trọng
| Phương thức | Công dụng |
|---|---|
Storage::put() | Lưu nội dung |
Storage::get() | Đọc file |
Storage::exists() | Kiểm tra tồn tại |
Storage::delete() | Xóa file |
Storage::copy() | Copy file |
Storage::move() | Di chuyển file |
Storage::url() | Tạo URL |
Storage::download() | Download |
Storage::temporaryUrl() | URL có thời hạn |
Storage::files() | Liệt kê file |
Storage::allFiles() | Liệt kê toàn bộ file |
Storage::makeDirectory() | Tạo thư mục |
Storage::deleteDirectory() | Xóa thư mục |
Storage::size() | Kích thước file |
Storage::mimeType() | MIME type |
Storage::lastModified() | Thời gian sửa cuối |
$file->store() | Upload file |
$file->storeAs() | Upload với tên chỉ định |
48. Tổng kết
Trong bài này chúng ta đã tìm hiểu:
Storage là gì.
Filesystem của Laravel.
Disk.
Local Storage.
Public Storage.
storage:link.StorageFacade.put().get().exists().store().storeAs().hashName().Upload Image.
Storage URL.
Delete File.
Copy File.
Move File.
Directory.
File Metadata.
Public / Private Visibility.
Download.
Temporary URL.
Amazon S3.
S3-Compatible Storage.
Storage Testing.
Read-Through Filesystem trong Laravel 13.
Ứng dụng Storage vào Blog CMS.
🎯 BÀI TẬP THỰC HÀNH
Bài tập 1 — Upload ảnh Post
Tạo Form:
<input
type="file"
name="image"
>
Upload vào:
storage/app/public/posts
Bài tập 2 — Hiển thị ảnh
Sử dụng:
Storage::url($post->image)
để hiển thị ảnh trong Blog.
Bài tập 3 — Thay ảnh
Khi Admin upload ảnh mới:
Xóa ảnh cũ
↓
Upload ảnh mới
↓
Update Database
Bài tập 4 — Xóa Post
Khi xóa Post:
Xóa Image
↓
Xóa Post
Bài tập 5 — Kiểm tra Storage
Chạy:
php artisan storage:link
Sau đó kiểm tra:
storage/app/public
và:
public/storage
💡 GHI NHỚ
Database lưu thông tin về file.
Storage lưu file thật.
Public Disk dành cho những file cần truy cập công khai.
Private Storage dành cho những file cần kiểm soát quyền truy cập.
Khi thay hoặc xóa file, đừng quên xử lý file vật lý trong Storage.
Đối với Blog CMS:
POST
│
┌──────────┴──────────┐
│ │
Database Storage
│ │
▼ ▼
image = posts/a.jpg posts/a.jpg
Đây là cách Laravel tách biệt:
DATA
và:
FILES
giúp ứng dụng dễ bảo trì và sau này có thể chuyển từ Local Storage sang Cloud Storage mà không phải viết lại toàn bộ chức năng upload.
Bài tiếp theo: Bài 40 — Logging & Debug, tìm hiểu Log, Debugbar và Telescope để theo dõi, debug và quan sát ứng dụng Laravel trong quá trình phát triển.
x1
quay về MỤC LỤC




Không có nhận xét nào:
Đăng nhận xét