Оформление заказа и транзакции базы данных
Сохранение данных заказа и связанных товаров в БД с обеспечением атомарности операций через DB::transaction. Финализация основного функционала магазина.
Мы подошли к кульминации разработки интернет-магазина — процессу оформления заказа (Checkout). В этот момент временные данные из сессионной корзины должны превратиться в постоянные записи в базе данных. Процесс состоит из нескольких шагов: создание записи в таблице orders, создание нескольких записей в таблице order_items (по одной на каждый товар в корзине) и очистка корзины. Здесь кроется огромная опасность. Представьте ситуацию: заказ создан, первый товар добавлен в order_items, а при добавлении второго товара сервер падает (например, сбой соединения с БД). В результате в базе остается 'сломанный' заказ без части товаров. Чтобы избежать подобных катастроф, используются Транзакции базы данных. Транзакция гарантирует свойство Атомарности (Atomicity) — 'всё или ничего'. Либо все SQL-запросы внутри транзакции выполняются успешно, и данные фиксируются (Commit), либо при малейшей ошибке происходит откат (Rollback) к исходному состоянию, словно никаких запросов и не было.
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use App\Models\Order;
use App\Models\OrderItem;
use Illuminate\Http\Request;
class CheckoutController extends Controller
{
public function process(Request $request)
{
$cart = session('cart', []);
if (empty($cart)) {
return redirect()->route('cart.index')->with('error', 'Корзина пуста');
}
// Запуск транзакции базы данных
DB::transaction(function () use ($cart) {
// 1. Считаем общую сумму
$total = array_sum(array_map(fn($item) => $item['price'] * $item['quantity'], $cart));
// 2. Создаем заказ
$order = Order::create([
'user_id' => auth()->id(), // Пользователь должен быть авторизован (middleware 'auth')
'total_price' => $total,
'status' => 'pending'
]);
// 3. Создаем позиции заказа
foreach ($cart as $id => $item) {
OrderItem::create([
'order_id' => $order->id,
'product_id' => $id,
'quantity' => $item['quantity'],
'price' => $item['price'] // Фиксируем цену на момент покупки
]);
}
});
// Если код дошел сюда, транзакция успешна. Очищаем корзину.
session()->forget('cart');
return redirect()->route('orders.success')->with('success', 'Заказ успешно оформлен!');
}
}
Разберем код контроллера, опираясь на материалы наших упражнений. Метод DB::transaction принимает анонимную функцию (Closure). Важно: поскольку анонимная функция в PHP имеет свою область видимости, мы должны передать внутрь нее переменную $cart с помощью конструкции use ($cart). Внутри замыкания мы сначала вычисляем общую стоимость заказа (total). Затем создаем запись в таблице orders. Мы используем auth()->id(), потому что маршрут защищен middleware 'auth' (об этом мы говорили в 12 уроке). Получив объект $order, мы запускаем цикл foreach по корзине. Для каждого товара создается запись OrderItem, куда мы передаем $order->id в качестве внешнего ключа. Если на любом этапе (например, при сохранении OrderItem) возникнет исключение (Exception), Laravel автоматически перехватит его и выполнит SQL-команду ROLLBACK. База данных останется в чистоте. Если цикл завершается без ошибок, Laravel выполняет COMMIT.
| Действие при Checkout | Таблица | Результат |
|---|---|---|
| Создание заказа | orders | Новая строка: user_id, total_price, статус 'pending' |
| Копирование товаров | order_items | Новые строки: order_id, product_id, цена, количество |
| Очистка | сессия | Удаление ключа 'cart' |
Особое внимание обратите на сохранение цены в таблице order_items. Мы берем цену из сессии (которая была актуальна на момент добавления в корзину) и жестко записываем ее в БД. Никогда не делайте связь в заказе, которая динамически тянет цену из таблицы products. Почему? Потому что через год администратор магазина повысит цену на товар. Если вы будете динамически брать текущую цену, у пользователя в истории заказов за прошлый год цена тоже изменится, и общая сумма заказа 'поплывет'. Заказ — это исторический документ, он должен фиксировать состояние (snapshot) на момент сделки. Транзакции и историческая фиксация данных — это стандарты индустрии, которые отличают любительский код от профессионального production-решения.
Флеш-карточки
Что такое атомарность в контексте баз данных?
Нажмите, чтобы увидеть ответ
Принцип 'Всё или ничего'. Группа запросов выполняется либо полностью успешно, либо ни один из них не применяется.
Нажмите, чтобы вернуться
Зачем использовать конструкцию use ($cart) в анонимной функции DB::transaction?
Нажмите, чтобы увидеть ответ
Чтобы передать переменную $cart из области видимости контроллера внутрь области видимости анонимной функции.
Нажмите, чтобы вернуться