Файловое хранилище (File Storage)
Laravel предоставляет абстракцию файловой системы через пакет Flysystem, поддерживая локальное хранилище, Amazon S3, SFTP и другие драйверы с единым API.
Конфигурация дисков
// config/filesystems.php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
// Local disk - private files (storage/app/private)
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
],
// Public disk - publicly accessible (storage/app/public)
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
'throw' => false,
],
// Amazon S3
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
// SFTP
'sftp' => [
'driver' => 'sftp',
'host' => env('SFTP_HOST'),
'username' => env('SFTP_USERNAME'),
'password' => env('SFTP_PASSWORD'),
'privateKey' => env('SFTP_PRIVATE_KEY'),
'passphrase' => env('SFTP_PASSPHRASE'),
'root' => env('SFTP_ROOT', '/'),
'timeout' => 30,
],
],
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
Создание символической ссылки
php artisan storage:link
# Creates: public/storage → storage/app/public
Основные операции
Фасад Storage
use Illuminate\Support\Facades\Storage;
// Default disk
Storage::put('file.txt', 'Contents');
// Specific disk
Storage::disk('s3')->put('file.txt', 'Contents');
// Switch disk dynamically
$disk = Storage::disk($user->preferredStorage());
Чтение файлов
// Get file contents as string
$contents = Storage::get('documents/report.pdf');
// Check if file exists
if (Storage::exists('photos/avatar.jpg')) {
// File exists
}
// Check if file is missing
if (Storage::missing('photos/avatar.jpg')) {
// File does not exist
}
// Get file metadata
$size = Storage::size('documents/report.pdf'); // Size in bytes
$lastModified = Storage::lastModified('file.txt'); // Unix timestamp
$mimeType = Storage::mimeType('photos/avatar.jpg'); // image/jpeg
// Get all files in directory
$files = Storage::files('photos'); // Only top-level
$allFiles = Storage::allFiles('photos'); // Recursive
// Get all directories
$dirs = Storage::directories('uploads');
$allDirs = Storage::allDirectories('uploads'); // Recursive
Запись файлов
// Write string content
Storage::put('file.txt', 'File contents here');
// Write with visibility
Storage::put('file.txt', 'Content', 'public');
// Prepend/Append
Storage::prepend('log.txt', 'First line');
Storage::append('log.txt', 'New line at end');
// Store with auto-generated filename
$path = Storage::putFile('photos', $request->file('avatar'));
// Returns: photos/abc123def456.jpg
// Store with custom filename
$path = Storage::putFileAs('photos', $request->file('avatar'), 'user-avatar.jpg');
// Returns: photos/user-avatar.jpg
// Stream large files (memory efficient)
Storage::writeStream('large-file.zip', fopen('/local/path/large-file.zip', 'r'));
$stream = Storage::readStream('large-file.zip');
Копирование и перемещение
Storage::copy('old/file.jpg', 'new/file.jpg');
Storage::move('old/file.jpg', 'new/file.jpg');
Удаление файлов
// Delete single file
Storage::delete('file.txt');
// Delete multiple files
Storage::delete(['file1.txt', 'file2.txt', 'file3.txt']);
// Delete directory
Storage::deleteDirectory('uploads/temp');
// Create directory
Storage::makeDirectory('uploads/images');
Загрузка файлов (File Uploads)
Из HTTP-запроса
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
final class AvatarController extends Controller
{
public function store(Request $request): RedirectResponse
{
$request->validate([
'avatar' => ['required', 'image', 'max:2048', 'dimensions:min_width=100,min_height=100'],
]);
// Store with auto-generated name
$path = $request->file('avatar')->store('avatars', 'public');
// Store with custom name
$path = $request->file('avatar')->storeAs(
'avatars',
"user-{$request->user()->id}.jpg",
'public'
);
// Update user record
$request->user()->update(['avatar_path' => $path]);
return redirect()->back()->with('status', 'Avatar updated!');
}
}
Множественная загрузка
public function uploadPhotos(Request $request): RedirectResponse
{
$request->validate([
'photos' => ['required', 'array', 'max:10'],
'photos.*' => ['image', 'max:5120'],
]);
$paths = [];
foreach ($request->file('photos') as $photo) {
$paths[] = $photo->store('gallery/' . auth()->id(), 'public');
}
// Save paths to database
foreach ($paths as $path) {
Photo::create([
'user_id' => auth()->id(),
'path' => $path,
]);
}
return redirect()->back()->with('status', count($paths) . ' photos uploaded!');
}
Информация о загруженном файле
$file = $request->file('document');
$file->getClientOriginalName(); // Original filename
$file->getClientOriginalExtension(); // Original extension
$file->getClientMimeType(); // MIME type from client
$file->getMimeType(); // Actual MIME type (detected)
$file->getSize(); // Size in bytes
$file->extension(); // Guessed extension from MIME
$file->hashName(); // Auto-generated hash name
$file->path(); // Temp path on server
$file->isValid(); // Upload was successful
URL файлов
Публичные URL
// For 'public' disk
$url = Storage::disk('public')->url('avatars/user-1.jpg');
// https://example.com/storage/avatars/user-1.jpg
// For S3
$url = Storage::disk('s3')->url('photos/image.jpg');
// https://bucket.s3.amazonaws.com/photos/image.jpg
Временные URL (Temporary URLs)
Временные URL с ограниченным сроком действия. Доступны для S3 и других облачных драйверов:
// Generate temporary URL (expires in 5 minutes)
$url = Storage::disk('s3')->temporaryUrl(
'reports/confidential-report.pdf',
now()->addMinutes(5)
);
// Temporary URL with custom headers
$url = Storage::disk('s3')->temporaryUrl(
'documents/contract.pdf',
now()->addHour(),
[
'ResponseContentType' => 'application/octet-stream',
'ResponseContentDisposition' => 'attachment; filename="contract.pdf"',
]
);
Temporary URL для локального диска
В Laravel 11 можно генерировать временные URL для локальных файлов:
// config/filesystems.php
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true, // Enable serving via route
'throw' => false,
],
// Generates signed URL for local files
$url = Storage::disk('local')->temporaryUrl(
'private/invoice.pdf',
now()->addMinutes(30)
);
Видимость файлов (Visibility)
// Set visibility when storing
Storage::put('file.txt', 'Content', 'public');
// Change visibility
Storage::setVisibility('file.txt', 'public');
Storage::setVisibility('file.txt', 'private');
// Get visibility
$visibility = Storage::getVisibility('file.txt');
// 'public' or 'private'
Скачивание и Streaming
// Download response
return Storage::download('reports/monthly.pdf');
return Storage::download('reports/monthly.pdf', 'Custom-Name.pdf', [
'Content-Type' => 'application/pdf',
]);
// Stream response (for large files)
return Storage::response('videos/tutorial.mp4');
Практический пример: сервис управления файлами
declare(strict_types=1);
namespace App\Services;
use App\Models\Attachment;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
final class FileService
{
private const MAX_SIZE_MB = 50;
private const ALLOWED_MIMES = [
'image/jpeg', 'image/png', 'image/webp',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
public function upload(UploadedFile $file, string $directory, string $disk = 'public'): Attachment
{
$this->validateFile($file);
$filename = $this->generateFilename($file);
$path = $file->storeAs($directory, $filename, $disk);
return Attachment::create([
'disk' => $disk,
'path' => $path,
'filename' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'hash' => hash_file('sha256', $file->path()),
]);
}
public function delete(Attachment $attachment): bool
{
if (Storage::disk($attachment->disk)->exists($attachment->path)) {
Storage::disk($attachment->disk)->delete($attachment->path);
}
return $attachment->delete();
}
public function getUrl(Attachment $attachment, int $expiresMinutes = 60): string
{
$disk = Storage::disk($attachment->disk);
if ($attachment->disk === 's3') {
return $disk->temporaryUrl($attachment->path, now()->addMinutes($expiresMinutes));
}
if ($attachment->disk === 'public') {
return $disk->url($attachment->path);
}
return $disk->temporaryUrl($attachment->path, now()->addMinutes($expiresMinutes));
}
private function validateFile(UploadedFile $file): void
{
if (!$file->isValid()) {
throw new \RuntimeException('File upload failed.');
}
if (!in_array($file->getMimeType(), self::ALLOWED_MIMES, true)) {
throw new \RuntimeException('File type not allowed: ' . $file->getMimeType());
}
if ($file->getSize() > self::MAX_SIZE_MB * 1024 * 1024) {
throw new \RuntimeException('File size exceeds ' . self::MAX_SIZE_MB . 'MB limit.');
}
}
private function generateFilename(UploadedFile $file): string
{
return Str::uuid()->toString() . '.' . $file->extension();
}
}
Кастомные файловые системы
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use League\Flysystem\Filesystem;
final class FilesystemServiceProvider extends ServiceProvider
{
public function boot(): void
{
Storage::extend('dropbox', function ($app, $config) {
$adapter = new DropboxAdapter(
new DropboxClient($config['authorization_token'])
);
return new FilesystemAdapter(
new Filesystem($adapter, $config),
$adapter,
$config
);
});
}
}
// config/filesystems.php
'disks' => [
'dropbox' => [
'driver' => 'dropbox',
'authorization_token' => env('DROPBOX_TOKEN'),
],
],
Тестирование файлового хранилища
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
final class AvatarUploadTest extends TestCase
{
public function test_avatar_can_be_uploaded(): void
{
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg', 200, 200);
$response = $this->actingAs(User::factory()->create())
->post('/avatar', ['avatar' => $file]);
$response->assertRedirect();
// Assert file was stored
Storage::disk('public')->assertExists('avatars/' . $file->hashName());
}
public function test_non_image_file_rejected(): void
{
Storage::fake('public');
$file = UploadedFile::fake()->create('document.pdf', 1024, 'application/pdf');
$response = $this->actingAs(User::factory()->create())
->post('/avatar', ['avatar' => $file]);
$response->assertSessionHasErrors('avatar');
Storage::disk('public')->assertMissing('avatars/' . $file->hashName());
}
public function test_oversized_file_rejected(): void
{
Storage::fake('public');
// Create 5MB image (exceeds 2MB limit)
$file = UploadedFile::fake()->image('avatar.jpg')->size(5120);
$response = $this->actingAs(User::factory()->create())
->post('/avatar', ['avatar' => $file]);
$response->assertSessionHasErrors('avatar');
}
public function test_multiple_photos_uploaded(): void
{
Storage::fake('public');
$files = [
UploadedFile::fake()->image('photo1.jpg'),
UploadedFile::fake()->image('photo2.jpg'),
UploadedFile::fake()->image('photo3.jpg'),
];
$response = $this->actingAs($user = User::factory()->create())
->post('/photos', ['photos' => $files]);
$response->assertRedirect();
// Assert all files stored
foreach ($files as $file) {
Storage::disk('public')->assertExists('gallery/' . $user->id . '/' . $file->hashName());
}
}
}
::alert{type="info"} Storage::fake() создаёт виртуальную файловую систему в памяти. Файлы не записываются на диск. Это идеально для тестирования - быстро, безопасно, не оставляет мусора. ::