179 lines
5.3 KiB
PHP
179 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controller\Webhook;
|
|
|
|
use App\Message\MailjetNewsletterEventMessage;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Messenger\MessageBusInterface;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
final class MailjetNewsletterWebhookController extends AbstractController
|
|
{
|
|
private int $ignored = 0;
|
|
|
|
/**
|
|
* @param array<int, string> $mailjetLists
|
|
*/
|
|
public function __construct(
|
|
private readonly MessageBusInterface $messageBus,
|
|
private readonly LoggerInterface $logger,
|
|
private readonly array $mailjetLists = [],
|
|
) {
|
|
}
|
|
|
|
#[Route('/webhooks/mailjet/newsletter', name: 'app_webhook_mailjet_newsletter', methods: ['POST'])]
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$payload = json_decode($request->getContent(), true);
|
|
if (JSON_ERROR_NONE !== json_last_error() || false === is_array($payload)) {
|
|
return new JsonResponse(['success' => false, 'message' => 'Invalid JSON payload.'], Response::HTTP_BAD_REQUEST);
|
|
}
|
|
|
|
$events = $this->normalizeEvents($payload);
|
|
$dispatched = 0;
|
|
$this->ignored = 0;
|
|
|
|
foreach ($events as $eventPayload) {
|
|
$message = $this->createMessage($eventPayload);
|
|
if (null === $message) {
|
|
continue;
|
|
}
|
|
|
|
$this->messageBus->dispatch($message);
|
|
++$dispatched;
|
|
}
|
|
|
|
$this->logger->info('Processed Mailjet newsletter webhook payload', [
|
|
'events' => count($events),
|
|
'dispatched' => $dispatched,
|
|
'ignored' => $this->ignored,
|
|
]);
|
|
|
|
return new JsonResponse(['success' => true, 'dispatched' => $dispatched]);
|
|
}
|
|
|
|
/**
|
|
* Mailjet may send one event or a batch of events, so normalize both shapes
|
|
* into a list before dispatching.
|
|
*
|
|
* @param array<int|string, mixed> $payload
|
|
*
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function normalizeEvents(array $payload): array
|
|
{
|
|
if ([] === $payload) {
|
|
return [];
|
|
}
|
|
|
|
if (array_is_list($payload)) {
|
|
$events = [];
|
|
foreach ($payload as $entry) {
|
|
if (true === is_array($entry)) {
|
|
$events[] = $entry;
|
|
}
|
|
}
|
|
|
|
return $events;
|
|
}
|
|
|
|
return [$payload];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function createMessage(array $payload): ?MailjetNewsletterEventMessage
|
|
{
|
|
$event = isset($payload['event']) ? strtolower(trim((string) $payload['event'])) : '';
|
|
if (MailjetNewsletterEventMessage::EVENT_UNSUBSCRIBE !== $event) {
|
|
return null;
|
|
}
|
|
|
|
$email = isset($payload['email']) ? mb_strtolower(trim((string) $payload['email'])) : '';
|
|
if (false === filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
return null;
|
|
}
|
|
|
|
$mailjetListId = $this->resolveListId($payload);
|
|
if (null === $mailjetListId) {
|
|
return null;
|
|
}
|
|
|
|
if (false === $this->isHandledListId($mailjetListId)) {
|
|
++$this->ignored;
|
|
|
|
$this->logger->debug('Ignored Mailjet newsletter webhook event for unconfigured list', [
|
|
'email' => $email,
|
|
'mailjet_list_id' => $mailjetListId,
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
|
|
return new MailjetNewsletterEventMessage(
|
|
email: $email,
|
|
mailjetListId: $mailjetListId,
|
|
event: $event,
|
|
eventAt: $this->resolveEventAt($payload),
|
|
payload: $payload,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function resolveListId(array $payload): ?int
|
|
{
|
|
$value = $payload['mj_list_id'] ?? $payload['list_id'] ?? null;
|
|
if (true === is_int($value)) {
|
|
return $value > 0 ? $value : null;
|
|
}
|
|
|
|
if (true === is_string($value) && true === ctype_digit(trim($value))) {
|
|
$listId = (int) trim($value);
|
|
|
|
return $listId > 0 ? $listId : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function isHandledListId(int $mailjetListId): bool
|
|
{
|
|
return array_key_exists($mailjetListId, $this->mailjetLists);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
private function resolveEventAt(array $payload): ?\DateTimeImmutable
|
|
{
|
|
$value = $payload['time'] ?? $payload['event_at'] ?? null;
|
|
if (true === is_int($value) || true === is_float($value) || true === (is_string($value) && ctype_digit(trim($value)))) {
|
|
return (new \DateTimeImmutable())->setTimestamp((int) $value);
|
|
}
|
|
|
|
if (true === is_string($value) && '' !== trim($value)) {
|
|
try {
|
|
return new \DateTimeImmutable($value);
|
|
} catch (\Throwable $e) {
|
|
$this->logger->debug('Could not parse event timestamp from Mailjet webhook payload', [
|
|
'value' => $value,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|