Организация заказов в интернет-магазине на PHP
Основные подходы к обработке заказов в PHP
Как создать универсальный обработчик заказов с поддержкой транзакций?
Цель: обеспечить атомарность операций при создании заказа, чтобы избежать частичного сохранения данных. Этот подход применяется в большинстве проектов электронной коммерции.
Пример базового класса OrderHandler:
class OrderHandler {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function create(array $data): int {
$this->pdo->beginTransaction();
try {
$stmt = $this->pdo->prepare('INSERT INTO orders (user_id, total, status) VALUES (?, ?, ?)');
$stmt->execute([$data['user_id'], $data['total'], 'new']);
$orderId = (int) $this->pdo->lastInsertId();
$stmt = $this->pdo->prepare('INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)');
foreach ($data['items'] as $item) {
$stmt->execute([$orderId, $item['product_id'], $item['quantity'], $item['price']]);
}
$this->pdo->commit();
return $orderId;
} catch (\Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
public function updateStatus(int $orderId, string $status): void {
$stmt = $this->pdo->prepare('UPDATE orders SET status = ? WHERE id = ?');
$stmt->execute([$status, $orderId]);
}
}
Booking php (система бронирования на php)
Пояснение: методы используют подготовленные запросы для безопасности от SQL-инъекций. Транзакция гарантирует, что вставка заказа и его позиций происходит полностью или откатывается при ошибке.
Типичная ошибка:
Пропуск обработки внешних ключей или блокировок таблиц. При создании заказа в высоконагруженной системе может возникнуть взаимоблокировка. Решение: использовать правильные индексы и уровни изоляции транзакций (например, READ COMMITTED).
Как реализовать обработку заказа через REST API с помощью Slim Framework?
Цель: отделить логику заказов от клиентской части и предоставить унифицированный интерфейс для мобильных приложений и веб-клиентов.
Пример маршрута для создания заказа:
$app->post('/orders', function ($request, $response) {
$data = $request->getParsedBody();
$handler = new OrderHandler($this->get(PDO::class));
try {
$orderId = $handler->create($data);
return $response->withJson(['id' => $orderId], 201);
} catch (\Exception $e) {
return $response->withJson(['error' => $e->getMessage()], 400);
}
});
Orders php orders action (заказы в php)
Пояснение: создается экземпляр OrderHandler, вызывается метод create. При ошибке возвращается HTTP 400. Для авторизации и валидации данных добавляются middleware.
Типичная ошибка:
Отсутствие проверки прав доступа. Любой неавторизованный пользователь может создать заказ от чужого имени. Решение: добавить middleware аутентификации (JWT, сессии).
Как обрабатывать заказы асинхронно с использованием очередей (RabbitMQ)?
Цель: снизить нагрузку на веб-сервер при создании заказов, особенно если требуется выполнение длительных операций (отправка email, генерация PDF, обновление инвентаря).
Пример продюсера (отправка сообщения в очередь):
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('order_created', false, true, false, false);
$msg = new AMQPMessage(json_encode($orderData), ['delivery_mode' => 2]);
$channel->basic_publish($msg, '', 'order_created');
echo "Order message sent\n";
$channel->close();
$connection->close();
Product view php (просмотр товара php)
Пример консюмера:
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('order_created', false, true, false, false);
$callback = function ($msg) {
$order = json_decode($msg->body, true);
// выполнение обработки: запись в БД, отправка email и т.д.
echo "Order processed: " . $order['id'] . "\n";
$msg->ack();
};
$channel->basic_qos(null, 1, null);
$channel->basic_consume('order_created', '', false, false, false, false, $callback);
while ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
Catalogue php page (каталог на php)
Пояснение: продюсер отправляет JSON с данными заказа, консюмер обрабатывает его. Параметр delivery_mode=2 делает сообщение persistent.
Типичная ошибка:
Потеря сообщения при падении консюмера. Решение: использовать подтверждения (ack) и dead letter exchanges для обработки неудачных попыток.
Как интегрировать обработку платежей через Stripe с вебхуками?
Цель: получать уведомления об успешных или неуспешных платежах и изменять статус заказа.
Пример обработки вебхука checkout.session.completed:
$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = \Stripe\Webhook::constructEvent(
$payload, $sig_header, $endpoint_secret
);
if ($event->type === 'checkout.session.completed') {
$session = $event->data->object;
$orderId = $session->metadata->order_id;
$handler = new OrderHandler($pdo);
$handler->updateStatus($orderId, 'paid');
}
http_response_code(200);
Пояснение: верификация подписи обязательна для безопасности. Метаданные заказа передаются при создании сессии оплаты.
Типичная ошибка:
Игнорирование повторной отправки вебхука Stripe. Один и тот же вебхук может быть отправлен несколько раз. Решение: идемпотентность - проверить, что заказ уже имеет статус paid, иначе обновлять.
Как управлять статусами заказов с помощью конечного автомата (state machine)?
Цель: четко определить допустимые переходы и избежать некорректных изменений статуса (например, из 'cancelled' в 'paid').
Пример с использованием библиотеки PHP State Machine:
$stateMachine = new \StateMachine\StateMachine([
'new' => ['pay' => 'paid', 'cancel' => 'cancelled'],
'paid' => ['ship' => 'shipped', 'refund' => 'refunded'],
'shipped' => ['deliver' => 'delivered'],
'cancelled' => [],
'refunded' => [],
]);
$order = ['status' => 'new'];
$transition = 'pay';
if ($stateMachine->can($order['status'], $transition)) {
$order['status'] = $stateMachine->apply($order['status'], $transition);
// обновить БД
}
Пояснение: подход предотвращает выполнение недопустимых операций и упрощает добавление новых статусов.
Типичная ошибка:
Сложность при интеграции с внешними системами (например, если статус изменяется извне). Решение: все изменения статуса только через state machine, внешние вызовы обрабатывать через API.
Как вести лог действий с заказами для аудита с использованием Monolog?
Цель: иметь возможность отследить, кто и когда изменил заказ, для решения спорных ситуаций и безопасности.
Пример логирования при изменении статуса:
$logger = new Logger('orders');
$logger->pushHandler(new StreamHandler(__DIR__.'/logs/orders.log', Logger::INFO));
$logger->info('Order status changed', [
'order_id' => $orderId,
'from' => $oldStatus,
'to' => $newStatus,
'user_id' => $currentUserId,
'ip' => $_SERVER['REMOTE_ADDR']
]);
Пояснение: логируем не только событие, но и контекст. Журнал можно анализировать.
Типичная ошибка:
Запись больших объемов данных в лог без ротации. Решение: настроить ротацию логов (например, RotatingFileHandler).
Расширенные примеры кода для обработки заказов
Первый пример: полный класс OrderManager с методами получения заказов по ID, списка заказов пользователя и статистики.
class OrderManager {
private PDO $pdo;
public function __construct(PDO $pdo) { $this->pdo = $pdo; }
public function getOrder(int $id): ?array {
$stmt = $this->pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$id]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) return null;
$stmt = $this->pdo->prepare('SELECT * FROM order_items WHERE order_id = ?');
$stmt->execute([$id]);
$order['items'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $order;
}
public function getOrdersByUser(int $userId): array {
$stmt = $this->pdo->prepare('SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getOrderStats(): array {
$stmt = $this->pdo->query('SELECT status, COUNT(*) as count, SUM(total) as total FROM orders GROUP BY status');
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
Результат вызова getOrder(123):
Array
(
[id] => 123
[user_id] => 1
[total] => 299.99
[status] => paid
[created_at] => 2024-03-15 10:30:00
[items] => Array
(
[0] => Array
(
[id] => 1
[order_id] => 123
[product_id] => 45
[quantity] => 2
[price] => 150.00
)
)
)
Второй пример: обработка вебхука Stripe с проверкой идемпотентности через базу данных.
$eventId = $event->id;
$stmt = $pdo->prepare('SELECT 1 FROM processed_events WHERE event_id = ?');
$stmt->execute([$eventId]);
if ($stmt->fetch()) {
http_response_code(200);
exit; // уже обработано
}
// обработка события
$stmt = $pdo->prepare('INSERT INTO processed_events (event_id, processed_at) VALUES (?, NOW())');
$stmt->execute([$eventId]);
$pdo->commit();
Результат: повторный вызов вебхука не приведет к двойной оплате.
Третий пример: асинхронная обработка с использованием Redis очереди (через list).
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$orderData = json_encode(['order_id' => 123, 'action' => 'send_confirmation_email']);
$redis->rPush('order_queue', $orderData); // продюсер
// консюмер:
while ($data = $redis->blPop('order_queue', 5)) {
$process = json_decode($data[1], true);
// выполнить действие
}
Результат: данные помещаются в очередь, консюмер извлекает и обрабатывает. Пример с использованием Supervisor для обеспечения постоянной работы.