Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Файли

Питання з реальних співбесід з відповідями: 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().

Докладніше в документації: Тимчасові URL для завантаження

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 лише перевіряв права.

Докладніше в документації: Потокові завантаження