Реалізація системи скачування файлів після оплати
Система скачування після оплати — критичний шлях в продажу цифрових товарів. Файл має стати доступним протягом секунд після підтвердження платежу, посилання має бути одноразовим або обмеженим, а сам файл — ніколи не видаватися прямо з публічної директорії.
Потік даних
Платіж підтвердив (webhook від еквайєра)
↓
PaymentController::webhook()
↓
PaymentConfirmedEvent → CreateDownloadLinksListener
↓
foreach (order.items as item where item.is_digital):
CreateDigitalDownloadAction::execute(item)
→ DigitalOrderDownload (token, limits, expiry)
↓
DigitalDownloadReadyMail → покупець отримує email
↓
Покупець кліває посилання → /download/{token}
↓
DigitalDownloadController::download(token)
→ валідація токену
→ стріминг файлу
Обробка webhook платіжної системи
class PaymentWebhookController
{
public function handle(Request $request, string $provider): JsonResponse
{
$handler = PaymentHandlerFactory::make($provider);
// Верифікуємо підпис вебхука
if (!$handler->verifySignature($request)) {
Log::warning('Invalid payment webhook signature', ['provider' => $provider]);
abort(400);
}
$paymentResult = $handler->parse($request);
if ($paymentResult->isSuccessful()) {
$order = Order::where('payment_id', $paymentResult->transactionId)->firstOrFail();
DB::transaction(function () use ($order, $paymentResult) {
$order->update([
'status' => 'paid',
'paid_at' => now(),
'payment_id' => $paymentResult->transactionId,
]);
event(new PaymentConfirmedEvent($order));
});
}
return response()->json(['ok' => true]);
}
}
Синхронне vs. асинхронне створення посилань
Синхронно (inline у Listener) — покупець отримує email протягом 1–2 секунд після оплати. Підходить для малої кількості позицій.
Асинхронно (через Queue) — надійніше при високому навантаженні. Email може затриматися на кілька секунд, але не затримає HTTP-відповідь вебхука.
class CreateDownloadLinksListener implements ShouldQueue
{
public $queue = 'digital-downloads';
public $tries = 5;
public $backoff = [5, 15, 30, 60, 120];
public function handle(PaymentConfirmedEvent $event): void
{
$order = $event->order;
$digitalItems = $order->items->filter(
fn($item) => $item->product->digitalProduct !== null
);
foreach ($digitalItems as $item) {
app(CreateDigitalDownloadAction::class)->execute($item);
}
}
}
Стріминг великих файлів
При виданні файлів з PHP важливо не завантажити весь файл у пам'ять. Laravel Storage::download() використовує стріминг автоматично, але для дуже великих файлів (>500 МБ) краще використовувати X-Accel-Redirect (nginx) або presigned URL (S3):
// Варіант 1: X-Accel-Redirect (nginx видає файл напряму, PHP тільки авторизує)
public function downloadViaAccel(DigitalOrderDownload $download): Response
{
$this->validateDownload($download);
$this->recordDownload($download);
$internalPath = '/private-files/' . $download->digitalProduct->storage_path;
return response('', 200, [
'X-Accel-Redirect' => $internalPath,
'Content-Type' => $download->digitalProduct->mime_type,
'Content-Disposition' => 'attachment; filename="' . $download->digitalProduct->original_filename . '"',
'X-Content-Type-Options' => 'nosniff',
]);
}
# nginx конфіг
location /private-files/ {
internal;
alias /var/www/storage/app/private/;
}
// Варіант 2: S3 Presigned URL (для великих файлів, CDN-видача)
public function downloadViaS3(DigitalOrderDownload $download): RedirectResponse
{
$this->validateDownload($download);
$this->recordDownload($download);
$url = Storage::disk('s3')->temporaryUrl(
path: $download->digitalProduct->storage_path,
expiration: now()->addMinutes(10),
options: [
'ResponseContentDisposition' => sprintf(
'attachment; filename="%s"',
$download->digitalProduct->original_filename
),
]
);
return redirect($url);
}
Email з посиланням на скачування
class DigitalDownloadReadyMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
private DigitalOrderDownload $download,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Ваш цифровий продукт готовий до скачування',
);
}
public function content(): Content
{
return new Content(
view: 'mails.digital-download-ready',
with: [
'downloadUrl' => route('downloads.show', $this->download->token),
'expiresAt' => $this->download->expires_at,
'remainingDownloads' => $this->download->remaining_downloads,
],
);
}
}
Графік реалізації
Базова реалізація: обробка webhook + одноразові посилання + email — 2–3 дні. Повна система: кілька типів файлів, стріминг, presigned URLs, обмеження скачувань, керування терміном дії — 4–6 днів.







