<?php
namespace App\Controller;
use App\Service\Api\Subscription\SubscriptionService;
use App\Service\Api\UsersProfile\Webshop\PaymentService;
use Doctrine\DBAL\Exception\DeadlockException;
use Psr\Log\LoggerInterface;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Webhook;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class WebhookController extends AbstractController
{
private $subscriptionService;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var PaymentService
*/
private $paymentService;
public function __construct(
SubscriptionService $subscriptionService,
LoggerInterface $logger,
PaymentService $paymentService
)
{
$this->subscriptionService = $subscriptionService;
$this->logger = $logger;
$this->paymentService = $paymentService;
}
/**
* @Route("/webhook/stripe", name="stripe_webhook", methods={"POST"})
*/
public function handleStripeWebhook(Request $request): Response
{
$payload = $request->getContent();
$sigHeader = $request->headers->get('Stripe-Signature');
$endpointSecret = $_ENV['STRIPE_WEBHOOK_SECRET'];
try {
$event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
} catch (SignatureVerificationException $e) {
$this->logger->error('Webhook signature verification failed.', ['error' => $e->getMessage()]);
return new Response('', 400);
}
try {
$this->processWebhookWithRetry(function() use ($event) {
switch ($event->type) {
case 'customer.subscription.updated':
$this->subscriptionService->handleSubscriptionUpdated($event->data->object);
break;
case 'invoice.paid':
$this->subscriptionService->handleInvoicePaid($event->data->object);
break;
case 'invoice.payment_failed':
$this->subscriptionService->handleInvoicePaymentFailed($event->data->object);
break;
case 'customer.subscription.deleted':
$this->subscriptionService->handleSubscriptionCanceled($event->data->object);
break;
case 'payment_intent.succeeded':
$this->paymentService->distributedPayment(
$event->data->object->id,
$event->data->object->latest_charge,
);
break;
default:
$this->logger->info('Unhandled event type: ' . $event->type);
return new Response('Unhandled event type: ' . $event->type, 200);
}
});
} catch (\Exception $e) {
$this->logger->error('Error processing webhook', [
'event' => $event->type,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return new Response('Webhook processing failed: ' . $e->getMessage(), 500);
}
return new Response('Webhook processed successfully', 200);
}
/**
* @throws DeadlockException
*/
private function processWebhookWithRetry(callable $process, $maxRetries = 3, $delay = 100000): void // 100000 microseconds = 0.1 seconds
{
$attempts = 0;
while ($attempts < $maxRetries) {
try {
$process();
return;
} catch (DeadlockException $e) {
$attempts++;
if ($attempts >= $maxRetries) {
throw $e;
}
usleep($delay * $attempts); // Exponential backoff
}
}
}
}