Реалізація збереження даних форми в базі та відправки на email
Будь-яка форма на сайті — зворотний зв'язок, заявка, опитування — потребує двох речей: надійного зберігання і оперативного сповіщення. Зберігати лише в базу — ризик пропустити заявку при збої пошти. Слати лише на email — втратити дані при проблемах з SMTP. Правильна схема робить обидва канали незалежними.
Структура таблиці
Мінімальна таблиця для зберігання заявок з форми:
CREATE TABLE form_submissions (
id BIGSERIAL PRIMARY KEY,
form_type VARCHAR(64) NOT NULL,
payload JSONB NOT NULL,
ip INET,
user_agent TEXT,
sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_form_submissions_form_type ON form_submissions(form_type);
CREATE INDEX idx_form_submissions_created_at ON form_submissions(created_at DESC);
payload в JSONB дозволяє зберігати довільну структуру без міграцій при кожній зміні форми. sent_at — мітка успішної відправки email, NULL означає «ще не відправлено» або «відправка упала».
Серверна обробка (PHP/Laravel)
// app/Http/Controllers/FormController.php
public function submit(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'nullable|string|max:32',
'message' => 'required|string|max:4000',
]);
// 1. Зберігаємо в базу відразу — незалежно від пошти
$submission = FormSubmission::create([
'form_type' => 'contact',
'payload' => $validated,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// 2. Відправляємо email через чергу
Mail::to(config('mail.admin_address'))
->queue(new FormSubmissionMail($submission));
return response()->json(['ok' => true]);
}
Ключовий момент: ->queue() замість ->send(). Черга означає, що збій SMTP не вернє 500 користувачу — заявка вже в базі, лист уйде при наступній спробі.
Mailable
// app/Mail/FormSubmissionMail.php
class FormSubmissionMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public FormSubmission $submission) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Нова заявка: ' . $this->submission->form_type,
replyTo: [
new Address($this->submission->payload['email'],
$this->submission->payload['name']),
],
);
}
public function content(): Content
{
return new Content(view: 'emails.form-submission');
}
}
replyTo заповнюється з даних користувача — менеджер натискає «Відповідь» і лист уходит сразу на клієнта, а не на no-reply адресу сайту.
Отмітка про успішну відправку
Слухач події MessageSent обновляет поле sent_at:
// app/Listeners/MarkSubmissionSent.php
public function handle(MessageSent $event): void
{
$message = $event->message;
// извлекаємо submission_id із заголовка X-Submission-Id
$id = $message->getHeaders()->get('X-Submission-Id')?->getValue();
if ($id) {
FormSubmission::where('id', $id)
->whereNull('sent_at')
->update(['sent_at' => now()]);
}
}
Заявки з sent_at = NULL можна періодично перепосилати через Artisan-команду або переглядати в адміністративній панелі.
Повторна відправка упалих листів
// app/Console/Commands/RetryUnsentSubmissions.php
// Запускається щих 15 хвилин через планувальник
$submissions = FormSubmission::whereNull('sent_at')
->where('created_at', '<', now()->subMinutes(5))
->limit(50)
->get();
foreach ($submissions as $submission) {
Mail::to(config('mail.admin_address'))
->queue(new FormSubmissionMail($submission));
}
Захист від спама
CSRF-токен обов'язковий за замовчуванням в Laravel. Додатково — honeypot-поле та rate limiting:
Route::post('/contact', FormController::class)
->middleware(['throttle:5,1']); // 5 запитів за хвилину з одного IP
Для високонавантажених форм — reCAPTCHA v3 або Turnstile від Cloudflare (без видимої капчи).
Що налаштовується
- Таблиця та індекси під конкретну схему даних
- Шаблон листа в HTML з брендингом клієнта
- Налаштування SMTP/Mailgun/SES в
.env - Supervisor для обробки черги
- Дашборд перегляду заявок в адміністраціï (опціонально)
Строк реалізації базової версії — 1 робочий день. З дашбордом перегляду заявок і повторною відправкою — 2–3 дні.







