Питання на співбесіді: Файли
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
6 питань
Файл приходить через $request->file(); зберігають через фасад Storage:
$request->validate([
'avatar' => ['required', 'image', 'max:2048'], // до 2 МБ
]);
$path = $request->file('avatar')->store('avatars', 'public');
// або з випадковим унікальним іменем - store() уже так робить
$user->update(['avatar_path' => $path]);
store()повертає шлях для збереження в БД.- Публічний URL -
Storage::url($path)(потребуєphp artisan storage:link). - Валідатори
image,mimes:pdf,docx,max:(в КБ) убезпечують від небажаних файлів.
Laravel абстрагує файлові операції через фасад Storage поверх Flysystem. «Диски» (local, public, s3) налаштовуються в config/filesystems.php.
Storage::disk('s3')->put('reports/q1.pdf', $contents);
$url = Storage::disk('s3')->url('reports/q1.pdf');
$temporary = Storage::disk('s3')->temporaryUrl('reports/q1.pdf', now()->addMinutes(5));
php artisan storage:linkстворює символічне посиланняpublic/storage → storage/app/publicдля публічного доступу.- Зміна сховища (локально ↔ S3) не вимагає переписування коду - лише конфіг.
Через тимчасовий підписаний URL для завантаження: сервер лише видає дозвіл, а файл іде з браузера чи мобільного застосунку напряму в сховище.
public function store(Request $request): JsonResponse
{
$path = 'uploads/'.$request->user()->id.'/'.Str::uuid().'.mp4';
['url' => $url, 'headers' => $headers] = Storage::disk('s3')->temporaryUploadUrl(
$path, now()->plus(minutes: 10),
);
return response()->json(compact('url', 'headers', 'path'));
}
Клієнт робить PUT на url з headers, потім повідомляє застосунку path.
Чому це краще за завантаження через PHP:
- великий файл не займає воркер PHP і не впирається в
upload_max_filesize,post_max_sizeі таймаути; - немає подвійного трафіку «клієнт → сервер → S3».
Що контролює сервер:
- шлях генерує сам (а не приймає від клієнта) - користувач пише лише у свою теку;
- короткий термін дії URL;
- після завантаження - перевірка: файл існує, розмір і тип відповідають очікуванням (
Storage::size(),mimeType()), лише тоді запис у базі; - незавершені завантаження прибирає правило життєвого циклу бакета.
Підтримують драйвери s3 і local. Для перегляду приватних файлів - temporaryUrl().
Storage::fake() підміняє диск тимчасовою текою, а UploadedFile::fake() створює файл-заглушку.
it('stores the avatar', function () {
Storage::fake('public');
$user = User::factory()->create();
$this->actingAs($user)
->post('/profile/avatar', ['avatar' => UploadedFile::fake()->image('me.jpg', 400, 400)])
->assertRedirect();
Storage::disk('public')->assertExists($user->fresh()->avatar_path);
});
it('rejects a file that is too big', function () {
Storage::fake('public');
$this->actingAs(User::factory()->create())
->post('/profile/avatar', ['avatar' => UploadedFile::fake()->create('big.jpg', 5000)])
->assertInvalid(['avatar']);
Storage::disk('public')->assertDirectoryEmpty('avatars');
});
Що варто знати:
fake()->image()створює справжнє зображення (потрібен GD) - пройдеimageіdimensions;fake()->create('doc.pdf', 1024, 'application/pdf')- файл заданого розміру в кілобайтах і MIME-типу, без реального вмісту;- перевірки:
assertExists,assertMissing,assertCount,assertDirectoryEmpty; - шлях краще брати з моделі, а не вгадувати: збережене ім'я генерується (
hashName).
Не забути негативні випадки: завеликий файл, неправильний тип, чужий користувач не може видалити файл. Саме там зазвичай діри.
Файл, покладений у storage/app, недоступний ззовні - це правильно за замовчуванням. Питання в тому, як віддати його тому, кому можна.
Що робити не варто: класти приватні файли в public/ чи публічний бакет і покладатися на те, що адресу ніхто не вгадає. Посилання розходяться, індексуються й лишаються робочими назавжди.
Спосіб 1 - віддати через контролер із перевіркою прав:
public function download(Document $document)
{
$this->authorize('view', $document);
return Storage::disk('private')->download($document->path, $document->name);
}
Кожне звернення проходить авторизацію. Мінус - файл іде крізь PHP, тож на великих файлах воркер зайнятий увесь час передачі.
Спосіб 2 - тимчасове підписане посилання до S3-сумісного сховища:
$url = Storage::disk('s3')->temporaryUrl($document->path, now()->addMinutes(5));
Посилання підписане й саме перестає працювати після строку. Файл віддає сховище напряму, PHP у передачі не бере участі - це те, що потрібно для великих файлів.
Спосіб 3 - підписаний маршрут, коли сховище тимчасових URL не вміє:
URL::temporarySignedRoute('documents.download', now()->addMinutes(5), ['document' => $document->id]);
Middleware signed перевіряє підпис і строк.
Про що часто забувають:
- Спосіб 1 без
authorize()перетворюється на публічний доступ через власний контролер. - Строк життя посилання має бути коротким: доки воно живе, ним може скористатися будь-хто, кому воно потрапило.
- Імʼя файла від користувача не можна підставляти в шлях -
Storage::putFile()генерує безпечне імʼя сам.
Головне правило - не тримати файл цілком у пам'яті PHP. Storage::get() на файлі в 2 ГБ упреться в memory_limit.
Запис потоком:
Storage::disk('s3')->putFile('videos', $request->file('video')); // потоком, а не get/put
Storage::disk('s3')->writeStream('backup.sql.gz', fopen($path, 'r'));
putFile() і putFileAs() передають файл потоком самі.
Читання потоком:
$stream = Storage::disk('s3')->readStream('exports/big.csv');
while (($line = fgets($stream)) !== false) { /* ... */ }
Віддача користувачу:
// згенерований на льоту файл - без запису на диск
return response()->streamDownload(function () {
$out = fopen('php://output', 'w');
Order::query()->lazyById(1000)->each(fn ($o) => fputcsv($out, [$o->id, $o->total]));
fclose($out);
}, 'orders.csv');
// готовий файл у S3 - краще посилання, ніж проксі через PHP
return redirect(Storage::disk('s3')->temporaryUrl($path, now()->plus(minutes: 5)));
Що ще допомагає:
- важку обробку (конвертація відео, архівування) - у чергу, з окремим воркером і більшим таймаутом;
- завантаження великих файлів від клієнтів - напряму в сховище (
temporaryUploadUrl()); - для
localу Nginx -X-Accel-Redirect, щоб файл віддавав вебсервер, а PHP лише перевіряв права.