$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 $payload * * @return list> */ 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 $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 $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 $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; } }