feat: MailJet list handling with DOI, webhook receiver and API endpoint

addresses #869cut134
This commit is contained in:
Björn Fromme
2026-04-29 09:51:57 +02:00
parent df7885ca1c
commit e4ef2f7adc
46 changed files with 3389 additions and 322 deletions
@@ -0,0 +1,139 @@
<?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
{
public function __construct(
private readonly MessageBusInterface $messageBus,
private readonly LoggerInterface $logger,
) {
}
#[Route('/webhooks/mailjet/newsletter', name: 'app_webhook_mailjet_newsletter', methods: ['POST'])]
public function __invoke(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;
foreach ($events as $eventPayload) {
$message = $this->createMessage($eventPayload);
if (null === $message) {
continue;
}
$this->messageBus->dispatch($message);
++$dispatched;
}
$this->logger->info('Accepted Mailjet newsletter webhook payload', [
'events' => count($events),
'dispatched' => $dispatched,
]);
return new JsonResponse(['success' => true, 'dispatched' => $dispatched]);
}
/**
* @param array<mixed> $payload
*
* @return list<array<string, mixed>>
*/
private function normalizeEvents(array $payload): array
{
if ([] === $payload) {
return [];
}
if (array_is_list($payload)) {
return array_values(array_filter($payload, static fn (mixed $entry): bool => is_array($entry)));
}
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;
}
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;
}
/**
* @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) {
return null;
}
}
return null;
}
}